From c18e0d1d37878bb4441f7f8d339cc23ac8e68448 Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Thu, 23 Feb 2023 09:02:10 +1100 Subject: [PATCH] feat(permissions): grant all permission for a group in permission prompt (#17140) This commit adds new "A" option to the interactive permission prompt, that will allow all subsequent permissions for given group (domain). Ie. when querying for permissions to access eg. env variables responding with "A" will allow access to all environmental variables. This works for all permission domains and should make permission prompts more ergonomic for users. --- cli/tests/integration/run_tests.rs | 56 +++ .../run/permissions_prompt_allow_all.ts | 20 + .../run/permissions_prompt_allow_all_2.ts | 8 + runtime/permissions/mod.rs | 399 +++++++++++------- runtime/permissions/prompter.rs | 31 +- 5 files changed, 363 insertions(+), 151 deletions(-) create mode 100644 cli/tests/testdata/run/permissions_prompt_allow_all.ts create mode 100644 cli/tests/testdata/run/permissions_prompt_allow_all_2.ts diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index d42136f84f..10ff6d2e0f 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -591,6 +591,62 @@ fn _090_run_permissions_request_sync() { ]); } +#[test] +fn permissions_prompt_allow_all() { + let args = "run --quiet run/permissions_prompt_allow_all.ts"; + use util::PtyData::*; + util::test_pty2(args, vec![ + // "run" permissions + Output("┌ ⚠️ Deno requests run access to \"FOO\".\r\n├ Requested by `Deno.permissions.query()` API\r\n ├ Run again with --allow-run to bypass this prompt.\r\n└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all run permissions) >"), + Input("a\n"), + Output("✅ Granted all run access.\r\n"), + // "read" permissions + Output("┌ ⚠️ Deno requests read access to \"FOO\".\r\n├ Requested by `Deno.permissions.query()` API\r\n ├ Run again with --allow-read to bypass this prompt.\r\n└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >"), + Input("a\n"), + Output("✅ Granted all read access.\r\n"), + // "write" permissions + Output("┌ ⚠️ Deno requests write access to \"FOO\".\r\n├ Requested by `Deno.permissions.query()` API\r\n ├ Run again with --allow-write to bypass this prompt.\r\n└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all write permissions) >"), + Input("a\n"), + Output("✅ Granted all write access.\r\n"), + // "net" permissions + Output("┌ ⚠️ Deno requests net access to \"FOO\".\r\n├ Requested by `Deno.permissions.query()` API\r\n ├ Run again with --allow-net to bypass this prompt.\r\n└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >"), + Input("a\n"), + Output("✅ Granted all net access.\r\n"), + // "env" permissions + Output("┌ ⚠️ Deno requests env access to \"FOO\".\r\n├ Requested by `Deno.permissions.query()` API\r\n ├ Run again with --allow-env to bypass this prompt.\r\n└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions) >"), + Input("a\n"), + Output("✅ Granted all env access.\r\n"), + // "sys" permissions + Output("┌ ⚠️ Deno requests sys access to \"loadavg\".\r\n├ Requested by `Deno.permissions.query()` API\r\n ├ Run again with --allow-sys to bypass this prompt.\r\n└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all sys permissions) >"), + Input("a\n"), + Output("✅ Granted all sys access.\r\n"), + // "ffi" permissions + Output("┌ ⚠️ Deno requests ffi access to \"FOO\".\r\n├ Requested by `Deno.permissions.query()` API\r\n ├ Run again with --allow-ffi to bypass this prompt.\r\n└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all ffi permissions) >"), + Input("a\n"), + Output("✅ Granted all ffi access.\r\n") + ]); +} + +#[test] +fn permissions_prompt_allow_all_2() { + let args = "run --quiet run/permissions_prompt_allow_all_2.ts"; + use util::PtyData::*; + util::test_pty2(args, vec![ + // "env" permissions + Output("┌ ⚠️ Deno requests env access to \"FOO\".\r\n├ Run again with --allow-env to bypass this prompt.\r\n└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions) >"), + Input("d\n"), + Output("✅ Granted all env access.\r\n"), + // "sys" permissions + Output("┌ ⚠️ Deno requests sys access to \"FOO\".\r\n├ Run again with --allow-sys to bypass this prompt.\r\n└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all sys permissions) >"), + Input("d\n"), + Output("✅ Granted all sys access.\r\n"), + // "read" permissions + Output("┌ ⚠️ Deno requests read access to \"FOO\".\r\n├ Run again with --allow-read to bypass this prompt.\r\n└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >"), + Input("d\n"), + Output("✅ Granted all read access.\r\n"), + ]); +} + itest!(_091_use_define_for_class_fields { args: "run --check run/091_use_define_for_class_fields.ts", output: "run/091_use_define_for_class_fields.ts.out", diff --git a/cli/tests/testdata/run/permissions_prompt_allow_all.ts b/cli/tests/testdata/run/permissions_prompt_allow_all.ts new file mode 100644 index 0000000000..8aa7d040ec --- /dev/null +++ b/cli/tests/testdata/run/permissions_prompt_allow_all.ts @@ -0,0 +1,20 @@ +Deno.permissions.requestSync({ name: "run", command: "FOO" }); +Deno.permissions.requestSync({ name: "run", command: "BAR" }); + +Deno.permissions.requestSync({ name: "read", path: "FOO" }); +Deno.permissions.requestSync({ name: "read", path: "BAR" }); + +Deno.permissions.requestSync({ name: "write", path: "FOO" }); +Deno.permissions.requestSync({ name: "write", path: "BAR" }); + +Deno.permissions.requestSync({ name: "net", host: "FOO" }); +Deno.permissions.requestSync({ name: "net", host: "BAR" }); + +Deno.permissions.requestSync({ name: "env", variable: "FOO" }); +Deno.permissions.requestSync({ name: "env", variable: "BAR" }); + +Deno.permissions.requestSync({ name: "sys", kind: "loadavg" }); +Deno.permissions.requestSync({ name: "sys", kind: "hostname" }); + +Deno.permissions.requestSync({ name: "ffi", path: "FOO" }); +Deno.permissions.requestSync({ name: "ffi", path: "BAR" }); diff --git a/cli/tests/testdata/run/permissions_prompt_allow_all_2.ts b/cli/tests/testdata/run/permissions_prompt_allow_all_2.ts new file mode 100644 index 0000000000..f42b357536 --- /dev/null +++ b/cli/tests/testdata/run/permissions_prompt_allow_all_2.ts @@ -0,0 +1,8 @@ +Deno.env.get("FOO"); +Deno.env.get("BAR"); + +Deno.loadavg(); +Deno.hostname(); + +Deno.cwd(); +Deno.lstatSync(new URL("../", import.meta.url)); diff --git a/runtime/permissions/mod.rs b/runtime/permissions/mod.rs index 2981463123..035c39304f 100644 --- a/runtime/permissions/mod.rs +++ b/runtime/permissions/mod.rs @@ -90,7 +90,7 @@ impl PermissionState { api_name: Option<&str>, info: Option<&str>, prompt: bool, - ) -> (Result<(), AnyError>, bool) { + ) -> (Result<(), AnyError>, bool, bool) { self.check2(name, api_name, || info.map(|s| s.to_string()), prompt) } @@ -101,11 +101,11 @@ impl PermissionState { api_name: Option<&str>, info: impl Fn() -> Option, prompt: bool, - ) -> (Result<(), AnyError>, bool) { + ) -> (Result<(), AnyError>, bool, bool) { match self { PermissionState::Granted => { Self::log_perm_access(name, info); - (Ok(()), false) + (Ok(()), false, false) } PermissionState::Prompt if prompt => { let msg = format!( @@ -113,14 +113,19 @@ impl PermissionState { name, info().map_or(String::new(), |info| { format!(" to {info}") }), ); - if PromptResponse::Allow == permission_prompt(&msg, name, api_name) { - Self::log_perm_access(name, info); - (Ok(()), true) - } else { - (Err(Self::error(name, info)), true) + match permission_prompt(&msg, name, api_name, true) { + PromptResponse::Allow => { + Self::log_perm_access(name, info); + (Ok(()), true, false) + } + PromptResponse::AllowAll => { + Self::log_perm_access(name, info); + (Ok(()), true, true) + } + PromptResponse::Deny => (Err(Self::error(name, info)), true, false), } } - _ => (Err(Self::error(name, info)), false), + _ => (Err(Self::error(name, info)), false, false), } } } @@ -161,6 +166,7 @@ impl UnitPermission { &format!("access to {}", self.description), self.name, Some("Deno.permissions.query()"), + false, ) { self.state = PermissionState::Granted; @@ -179,7 +185,7 @@ impl UnitPermission { } pub fn check(&mut self) -> Result<(), AnyError> { - let (result, prompted) = + let (result, prompted, _is_allow_all) = self.state.check(self.name, None, None, self.prompt); if prompted { if result.is_ok() { @@ -357,19 +363,26 @@ impl UnaryPermission { let (resolved_path, display_path) = resolved_and_display_path(path); let state = self.query(Some(&resolved_path)); if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - &format!("read access to \"{}\"", display_path.display()), - self.name, - Some("Deno.permissions.query()"), - ) - { - self.granted_list.insert(ReadDescriptor(resolved_path)); - PermissionState::Granted - } else { - self.denied_list.insert(ReadDescriptor(resolved_path)); - self.global_state = PermissionState::Denied; - PermissionState::Denied + match permission_prompt( + &format!("read access to \"{}\"", display_path.display()), + self.name, + Some("Deno.permissions.query()"), + true, + ) { + PromptResponse::Allow => { + self.granted_list.insert(ReadDescriptor(resolved_path)); + PermissionState::Granted + } + PromptResponse::Deny => { + self.denied_list.insert(ReadDescriptor(resolved_path)); + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + PromptResponse::AllowAll => { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + PermissionState::Granted + } } } else if state == PermissionState::Granted { self.granted_list.insert(ReadDescriptor(resolved_path)); @@ -385,6 +398,7 @@ impl UnaryPermission { "read access", self.name, Some("Deno.permissions.query()"), + true, ) { self.granted_list.clear(); @@ -421,7 +435,7 @@ impl UnaryPermission { path: &Path, api_name: Option<&str>, ) -> Result<(), AnyError> { - let (result, prompted) = self.query(Some(path)).check2( + let (result, prompted, is_allow_all) = self.query(Some(path)).check2( self.name, api_name, || Some(format!("\"{}\"", path.to_path_buf().display())), @@ -430,7 +444,12 @@ impl UnaryPermission { if prompted { let resolved_path = resolve_from_cwd(path)?; if result.is_ok() { - self.granted_list.insert(ReadDescriptor(resolved_path)); + if is_allow_all { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + } else { + self.granted_list.insert(ReadDescriptor(resolved_path)); + } } else { self.denied_list.insert(ReadDescriptor(resolved_path)); self.global_state = PermissionState::Denied; @@ -448,25 +467,33 @@ impl UnaryPermission { api_name: &str, ) -> Result<(), AnyError> { let resolved_path = resolve_from_cwd(path)?; - let (result, prompted) = self.query(Some(&resolved_path)).check( - self.name, - Some(api_name), - Some(&format!("<{display}>")), - self.prompt, - ); + let (result, prompted, is_allow_all) = + self.query(Some(&resolved_path)).check( + self.name, + Some(api_name), + Some(&format!("<{display}>")), + self.prompt, + ); if prompted { if result.is_ok() { - self.granted_list.insert(ReadDescriptor(resolved_path)); + if is_allow_all { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + } else { + self.granted_list.insert(ReadDescriptor(resolved_path)); + } } else { - self.denied_list.insert(ReadDescriptor(resolved_path)); self.global_state = PermissionState::Denied; + if !is_allow_all { + self.denied_list.insert(ReadDescriptor(resolved_path)); + } } } result } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { - let (result, prompted) = + let (result, prompted, _) = self .query(None) .check(self.name, api_name, Some("all"), self.prompt); @@ -530,19 +557,26 @@ impl UnaryPermission { let (resolved_path, display_path) = resolved_and_display_path(path); let state = self.query(Some(&resolved_path)); if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - &format!("write access to \"{}\"", display_path.display()), - self.name, - Some("Deno.permissions.query()"), - ) - { - self.granted_list.insert(WriteDescriptor(resolved_path)); - PermissionState::Granted - } else { - self.denied_list.insert(WriteDescriptor(resolved_path)); - self.global_state = PermissionState::Denied; - PermissionState::Denied + match permission_prompt( + &format!("write access to \"{}\"", display_path.display()), + self.name, + Some("Deno.permissions.query()"), + true, + ) { + PromptResponse::Allow => { + self.granted_list.insert(WriteDescriptor(resolved_path)); + PermissionState::Granted + } + PromptResponse::Deny => { + self.denied_list.insert(WriteDescriptor(resolved_path)); + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + PromptResponse::AllowAll => { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + PermissionState::Granted + } } } else if state == PermissionState::Granted { self.granted_list.insert(WriteDescriptor(resolved_path)); @@ -558,6 +592,7 @@ impl UnaryPermission { "write access", self.name, Some("Deno.permissions.query()"), + true, ) { self.granted_list.clear(); @@ -594,7 +629,7 @@ impl UnaryPermission { path: &Path, api_name: Option<&str>, ) -> Result<(), AnyError> { - let (result, prompted) = self.query(Some(path)).check2( + let (result, prompted, is_allow_all) = self.query(Some(path)).check2( self.name, api_name, || Some(format!("\"{}\"", path.to_path_buf().display())), @@ -603,7 +638,12 @@ impl UnaryPermission { if prompted { let resolved_path = resolve_from_cwd(path)?; if result.is_ok() { - self.granted_list.insert(WriteDescriptor(resolved_path)); + if is_allow_all { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + } else { + self.granted_list.insert(WriteDescriptor(resolved_path)); + } } else { self.denied_list.insert(WriteDescriptor(resolved_path)); self.global_state = PermissionState::Denied; @@ -613,7 +653,7 @@ impl UnaryPermission { } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { - let (result, prompted) = + let (result, prompted, _) = self .query(None) .check(self.name, api_name, Some("all"), self.prompt); @@ -685,19 +725,26 @@ impl UnaryPermission { let state = self.query(Some(host)); let host = NetDescriptor::new(&host); if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - &format!("network access to \"{host}\""), - self.name, - Some("Deno.permissions.query()"), - ) - { - self.granted_list.insert(host); - PermissionState::Granted - } else { - self.denied_list.insert(host); - self.global_state = PermissionState::Denied; - PermissionState::Denied + match permission_prompt( + &format!("network access to \"{host}\""), + self.name, + Some("Deno.permissions.query()"), + true, + ) { + PromptResponse::Allow => { + self.granted_list.insert(host); + PermissionState::Granted + } + PromptResponse::Deny => { + self.denied_list.insert(host); + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + PromptResponse::AllowAll => { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + PermissionState::Granted + } } } else if state == PermissionState::Granted { self.granted_list.insert(host); @@ -713,6 +760,7 @@ impl UnaryPermission { "network access", self.name, Some("Deno.permissions.query()"), + true, ) { self.granted_list.clear(); @@ -756,7 +804,7 @@ impl UnaryPermission { api_name: Option<&str>, ) -> Result<(), AnyError> { let new_host = NetDescriptor::new(&host); - let (result, prompted) = self.query(Some(host)).check( + let (result, prompted, is_allow_all) = self.query(Some(host)).check( self.name, api_name, Some(&format!("\"{new_host}\"")), @@ -764,7 +812,12 @@ impl UnaryPermission { ); if prompted { if result.is_ok() { - self.granted_list.insert(new_host); + if is_allow_all { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + } else { + self.granted_list.insert(new_host); + } } else { self.denied_list.insert(new_host); self.global_state = PermissionState::Denied; @@ -787,7 +840,7 @@ impl UnaryPermission { Some(port) => format!("{hostname}:{port}"), }; let host = &(&hostname, url.port_or_known_default()); - let (result, prompted) = self.query(Some(host)).check( + let (result, prompted, is_allow_all) = self.query(Some(host)).check( self.name, api_name, Some(&format!("\"{display_host}\"")), @@ -795,7 +848,12 @@ impl UnaryPermission { ); if prompted { if result.is_ok() { - self.granted_list.insert(NetDescriptor::new(&host)); + if is_allow_all { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + } else { + self.granted_list.insert(NetDescriptor::new(&host)); + } } else { self.denied_list.insert(NetDescriptor::new(&host)); self.global_state = PermissionState::Denied; @@ -805,7 +863,7 @@ impl UnaryPermission { } pub fn check_all(&mut self) -> Result<(), AnyError> { - let (result, prompted) = + let (result, prompted, _) = self .query::<&str>(None) .check(self.name, None, Some("all"), self.prompt); @@ -859,19 +917,26 @@ impl UnaryPermission { if let Some(env) = env { let state = self.query(Some(env)); if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - &format!("env access to \"{env}\""), - self.name, - Some("Deno.permissions.query()"), - ) - { - self.granted_list.insert(EnvDescriptor::new(env)); - PermissionState::Granted - } else { - self.denied_list.insert(EnvDescriptor::new(env)); - self.global_state = PermissionState::Denied; - PermissionState::Denied + match permission_prompt( + &format!("env access to \"{env}\""), + self.name, + Some("Deno.permissions.query()"), + true, + ) { + PromptResponse::Allow => { + self.granted_list.insert(EnvDescriptor::new(env)); + PermissionState::Granted + } + PromptResponse::Deny => { + self.denied_list.insert(EnvDescriptor::new(env)); + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + PromptResponse::AllowAll => { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + PermissionState::Granted + } } } else if state == PermissionState::Granted { self.granted_list.insert(EnvDescriptor::new(env)); @@ -887,6 +952,7 @@ impl UnaryPermission { "env access", self.name, Some("Deno.permissions.query()"), + true, ) { self.granted_list.clear(); @@ -915,7 +981,7 @@ impl UnaryPermission { } pub fn check(&mut self, env: &str) -> Result<(), AnyError> { - let (result, prompted) = self.query(Some(env)).check( + let (result, prompted, is_allow_all) = self.query(Some(env)).check( self.name, None, Some(&format!("\"{env}\"")), @@ -923,7 +989,12 @@ impl UnaryPermission { ); if prompted { if result.is_ok() { - self.granted_list.insert(EnvDescriptor::new(env)); + if is_allow_all { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + } else { + self.granted_list.insert(EnvDescriptor::new(env)); + } } else { self.denied_list.insert(EnvDescriptor::new(env)); self.global_state = PermissionState::Denied; @@ -933,7 +1004,7 @@ impl UnaryPermission { } pub fn check_all(&mut self) -> Result<(), AnyError> { - let (result, prompted) = + let (result, prompted, _) = self .query(None) .check(self.name, None, Some("all"), self.prompt); @@ -993,19 +1064,26 @@ impl UnaryPermission { } if let Some(kind) = kind { let desc = SysDescriptor(kind.to_string()); - if PromptResponse::Allow - == permission_prompt( - &format!("sys access to \"{kind}\""), - self.name, - Some("Deno.permissions.query()"), - ) - { - self.granted_list.insert(desc); - PermissionState::Granted - } else { - self.denied_list.insert(desc); - self.global_state = PermissionState::Denied; - PermissionState::Denied + match permission_prompt( + &format!("sys access to \"{kind}\""), + self.name, + Some("Deno.permissions.query()"), + true, + ) { + PromptResponse::Allow => { + self.granted_list.insert(desc); + PermissionState::Granted + } + PromptResponse::Deny => { + self.denied_list.insert(desc); + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + PromptResponse::AllowAll => { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + PermissionState::Granted + } } } else { if PromptResponse::Allow @@ -1013,6 +1091,7 @@ impl UnaryPermission { "sys access", self.name, Some("Deno.permissions.query()"), + true, ) { self.global_state = PermissionState::Granted; @@ -1041,7 +1120,7 @@ impl UnaryPermission { kind: &str, api_name: Option<&str>, ) -> Result<(), AnyError> { - let (result, prompted) = self.query(Some(kind)).check( + let (result, prompted, is_allow_all) = self.query(Some(kind)).check( self.name, api_name, Some(&format!("\"{kind}\"")), @@ -1049,7 +1128,12 @@ impl UnaryPermission { ); if prompted { if result.is_ok() { - self.granted_list.insert(SysDescriptor(kind.to_string())); + if is_allow_all { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + } else { + self.granted_list.insert(SysDescriptor(kind.to_string())); + } } else { self.denied_list.insert(SysDescriptor(kind.to_string())); self.global_state = PermissionState::Denied; @@ -1059,7 +1143,7 @@ impl UnaryPermission { } pub fn check_all(&mut self) -> Result<(), AnyError> { - let (result, prompted) = + let (result, prompted, _is_allow_all) = self .query(None) .check(self.name, None, Some("all"), self.prompt); @@ -1116,23 +1200,30 @@ impl UnaryPermission { if let Some(cmd) = cmd { let state = self.query(Some(cmd)); if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - &format!("run access to \"{cmd}\""), - self.name, - Some("Deno.permissions.query()"), - ) - { - self - .granted_list - .insert(RunDescriptor::from_str(cmd).unwrap()); - PermissionState::Granted - } else { - self - .denied_list - .insert(RunDescriptor::from_str(cmd).unwrap()); - self.global_state = PermissionState::Denied; - PermissionState::Denied + match permission_prompt( + &format!("run access to \"{cmd}\""), + self.name, + Some("Deno.permissions.query()"), + true, + ) { + PromptResponse::Allow => { + self + .granted_list + .insert(RunDescriptor::from_str(cmd).unwrap()); + PermissionState::Granted + } + PromptResponse::Deny => { + self + .denied_list + .insert(RunDescriptor::from_str(cmd).unwrap()); + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + PromptResponse::AllowAll => { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + PermissionState::Granted + } } } else if state == PermissionState::Granted { self @@ -1150,6 +1241,7 @@ impl UnaryPermission { "run access", self.name, Some("Deno.permissions.query()"), + true, ) { self.granted_list.clear(); @@ -1184,7 +1276,7 @@ impl UnaryPermission { cmd: &str, api_name: Option<&str>, ) -> Result<(), AnyError> { - let (result, prompted) = self.query(Some(cmd)).check( + let (result, prompted, is_allow_all) = self.query(Some(cmd)).check( self.name, api_name, Some(&format!("\"{cmd}\"")), @@ -1192,9 +1284,14 @@ impl UnaryPermission { ); if prompted { if result.is_ok() { - self - .granted_list - .insert(RunDescriptor::from_str(cmd).unwrap()); + if is_allow_all { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + } else { + self + .granted_list + .insert(RunDescriptor::from_str(cmd).unwrap()); + } } else { self .denied_list @@ -1206,7 +1303,7 @@ impl UnaryPermission { } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { - let (result, prompted) = + let (result, prompted, _) = self .query(None) .check(self.name, api_name, Some("all"), self.prompt); @@ -1267,19 +1364,26 @@ impl UnaryPermission { let (resolved_path, display_path) = resolved_and_display_path(path); let state = self.query(Some(&resolved_path)); if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - &format!("ffi access to \"{}\"", display_path.display()), - self.name, - Some("Deno.permissions.query()"), - ) - { - self.granted_list.insert(FfiDescriptor(resolved_path)); - PermissionState::Granted - } else { - self.denied_list.insert(FfiDescriptor(resolved_path)); - self.global_state = PermissionState::Denied; - PermissionState::Denied + match permission_prompt( + &format!("ffi access to \"{}\"", display_path.display()), + self.name, + Some("Deno.permissions.query()"), + true, + ) { + PromptResponse::Allow => { + self.granted_list.insert(FfiDescriptor(resolved_path)); + PermissionState::Granted + } + PromptResponse::Deny => { + self.denied_list.insert(FfiDescriptor(resolved_path)); + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + PromptResponse::AllowAll => { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + PermissionState::Granted + } } } else if state == PermissionState::Granted { self.granted_list.insert(FfiDescriptor(resolved_path)); @@ -1295,6 +1399,7 @@ impl UnaryPermission { "ffi access", self.name, Some("Deno.permissions.query()"), + true, ) { self.granted_list.clear(); @@ -1328,16 +1433,22 @@ impl UnaryPermission { pub fn check(&mut self, path: Option<&Path>) -> Result<(), AnyError> { if let Some(path) = path { let (resolved_path, display_path) = resolved_and_display_path(path); - let (result, prompted) = self.query(Some(&resolved_path)).check( - self.name, - None, - Some(&format!("\"{}\"", display_path.display())), - self.prompt, - ); + let (result, prompted, is_allow_all) = + self.query(Some(&resolved_path)).check( + self.name, + None, + Some(&format!("\"{}\"", display_path.display())), + self.prompt, + ); if prompted { if result.is_ok() { - self.granted_list.insert(FfiDescriptor(resolved_path)); + if is_allow_all { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + } else { + self.granted_list.insert(FfiDescriptor(resolved_path)); + } } else { self.denied_list.insert(FfiDescriptor(resolved_path)); self.global_state = PermissionState::Denied; @@ -1346,7 +1457,7 @@ impl UnaryPermission { result } else { - let (result, prompted) = + let (result, prompted, _) = self.query(None).check(self.name, None, None, self.prompt); if prompted { @@ -1362,7 +1473,7 @@ impl UnaryPermission { } pub fn check_all(&mut self) -> Result<(), AnyError> { - let (result, prompted) = + let (result, prompted, _) = self .query(None) .check(self.name, None, Some("all"), self.prompt); diff --git a/runtime/permissions/prompter.rs b/runtime/permissions/prompter.rs index c33483645f..d148b485e7 100644 --- a/runtime/permissions/prompter.rs +++ b/runtime/permissions/prompter.rs @@ -11,6 +11,7 @@ pub const PERMISSION_EMOJI: &str = "⚠️"; pub enum PromptResponse { Allow, Deny, + AllowAll, } static PERMISSION_PROMPTER: Lazy>> = @@ -26,11 +27,14 @@ pub fn permission_prompt( message: &str, flag: &str, api_name: Option<&str>, + is_unary: bool, ) -> PromptResponse { if let Some(before_callback) = MAYBE_BEFORE_PROMPT_CALLBACK.lock().as_mut() { before_callback(); } - let r = PERMISSION_PROMPTER.lock().prompt(message, flag, api_name); + let r = PERMISSION_PROMPTER + .lock() + .prompt(message, flag, api_name, is_unary); if let Some(after_callback) = MAYBE_AFTER_PROMPT_CALLBACK.lock().as_mut() { after_callback(); } @@ -53,6 +57,7 @@ pub trait PermissionPrompter: Send + Sync { message: &str, name: &str, api_name: Option<&str>, + is_unary: bool, ) -> PromptResponse; } @@ -64,6 +69,7 @@ impl PermissionPrompter for TtyPrompter { message: &str, name: &str, api_name: Option<&str>, + is_unary: bool, ) -> PromptResponse { if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) { return PromptResponse::Deny; @@ -198,7 +204,11 @@ impl PermissionPrompter for TtyPrompter { let _stderr_guard = std::io::stderr().lock(); // print to stderr so that if stdout is piped this is still displayed. - const OPTS: &str = "[y/n] (y = yes, allow; n = no, deny)"; + let opts: String = if is_unary { + format!("[y/n/A] (y = yes, allow; n = no, deny; A = allow all {name} permissions)") + } else { + "[y/n] (y = yes, allow; n = no, deny)".to_string() + }; eprint!("┌ {PERMISSION_EMOJI} "); eprint!("{}", colors::bold("Deno requests ")); eprint!("{}", colors::bold(message)); @@ -209,7 +219,7 @@ impl PermissionPrompter for TtyPrompter { let msg = format!("Run again with --allow-{name} to bypass this prompt."); eprintln!("├ {}", colors::italic(&msg)); eprint!("└ {}", colors::bold("Allow?")); - eprint!(" {OPTS} > "); + eprint!(" {opts} > "); let value = loop { let mut input = String::new(); let stdin = std::io::stdin(); @@ -221,24 +231,30 @@ impl PermissionPrompter for TtyPrompter { None => break PromptResponse::Deny, Some(v) => v, }; - match ch.to_ascii_lowercase() { - 'y' => { + match ch { + 'y' | 'Y' => { clear_n_lines(if api_name.is_some() { 4 } else { 3 }); let msg = format!("Granted {message}."); eprintln!("✅ {}", colors::bold(&msg)); break PromptResponse::Allow; } - 'n' => { + 'n' | 'N' => { clear_n_lines(if api_name.is_some() { 4 } else { 3 }); let msg = format!("Denied {message}."); eprintln!("❌ {}", colors::bold(&msg)); break PromptResponse::Deny; } + 'A' if is_unary => { + clear_n_lines(if api_name.is_some() { 4 } else { 3 }); + let msg = format!("Granted all {name} access."); + eprintln!("✅ {}", colors::bold(&msg)); + break PromptResponse::AllowAll; + } _ => { // If we don't get a recognized option try again. clear_n_lines(1); eprint!("└ {}", colors::bold("Unrecognized option. Allow?")); - eprint!(" {OPTS} > "); + eprint!(" {opts} > "); } }; }; @@ -264,6 +280,7 @@ pub mod tests { _message: &str, _name: &str, _api_name: Option<&str>, + _is_unary: bool, ) -> PromptResponse { if STUB_PROMPT_VALUE.load(Ordering::SeqCst) { PromptResponse::Allow