diff --git a/cli/dts/lib.deno.ns.d.ts b/cli/dts/lib.deno.ns.d.ts index 4934c642d6..09f39d6b83 100644 --- a/cli/dts/lib.deno.ns.d.ts +++ b/cli/dts/lib.deno.ns.d.ts @@ -1967,10 +1967,10 @@ declare namespace Deno { * * If `stdout` and/or `stderr` were set to `"piped"`, they must be closed * manually before the process can exit. - * + * * To run process to completion and collect output from both `stdout` and * `stderr` use: - * + * * ```ts * const p = Deno.run({ cmd, stderr: 'piped', stdout: 'piped' }); * const [status, stdout, stderr] = await Promise.all([ @@ -2135,6 +2135,7 @@ declare namespace Deno { export interface RunPermissionDescriptor { name: "run"; + command?: string; } export interface ReadPermissionDescriptor { diff --git a/cli/flags.rs b/cli/flags.rs index 6edce35de6..eb4bc86411 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -133,7 +133,7 @@ pub struct Flags { pub allow_net: Option>, pub allow_plugin: bool, pub allow_read: Option>, - pub allow_run: bool, + pub allow_run: Option>, pub allow_write: Option>, pub location: Option, pub cache_blocklist: Vec, @@ -211,8 +211,15 @@ impl Flags { args.push("--allow-env".to_string()); } - if self.allow_run { - args.push("--allow-run".to_string()); + match &self.allow_run { + Some(run_allowlist) if run_allowlist.is_empty() => { + args.push("--allow-run".to_string()); + } + Some(run_allowlist) => { + let s = format!("--allow-run={}", run_allowlist.join(",")); + args.push(s); + } + _ => {} } if self.allow_plugin { @@ -520,7 +527,7 @@ fn repl_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.subcommand = DenoSubcommand::Repl; flags.allow_net = Some(vec![]); flags.allow_env = true; - flags.allow_run = true; + flags.allow_run = Some(vec![]); flags.allow_read = Some(vec![]); flags.allow_write = Some(vec![]); flags.allow_plugin = true; @@ -531,7 +538,7 @@ fn eval_parse(flags: &mut Flags, matches: &clap::ArgMatches) { runtime_args_parse(flags, matches, false, true); flags.allow_net = Some(vec![]); flags.allow_env = true; - flags.allow_run = true; + flags.allow_run = Some(vec![]); flags.allow_read = Some(vec![]); flags.allow_write = Some(vec![]); flags.allow_plugin = true; @@ -1399,6 +1406,10 @@ fn permission_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .arg( Arg::with_name("allow-run") .long("allow-run") + .min_values(0) + .takes_value(true) + .use_delimiter(true) + .require_equals(true) .help("Allow running subprocesses"), ) .arg( @@ -1809,12 +1820,15 @@ fn permission_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) { debug!("net allowlist: {:#?}", &flags.allow_net); } + if let Some(run_wl) = matches.values_of("allow-run") { + let run_allowlist: Vec = run_wl.map(ToString::to_string).collect(); + flags.allow_run = Some(run_allowlist); + debug!("run allowlist: {:#?}", &flags.allow_run); + } + if matches.is_present("allow-env") { flags.allow_env = true; } - if matches.is_present("allow-run") { - flags.allow_run = true; - } if matches.is_present("allow-plugin") { flags.allow_plugin = true; } @@ -1825,7 +1839,7 @@ fn permission_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.allow_read = Some(vec![]); flags.allow_env = true; flags.allow_net = Some(vec![]); - flags.allow_run = true; + flags.allow_run = Some(vec![]); flags.allow_write = Some(vec![]); flags.allow_plugin = true; flags.allow_hrtime = true; @@ -2032,7 +2046,7 @@ mod tests { }, allow_net: Some(vec![]), allow_env: true, - allow_run: true, + allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), allow_plugin: true, @@ -2404,7 +2418,7 @@ mod tests { }, allow_net: Some(vec![]), allow_env: true, - allow_run: true, + allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), allow_plugin: true, @@ -2427,7 +2441,7 @@ mod tests { }, allow_net: Some(vec![]), allow_env: true, - allow_run: true, + allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), allow_plugin: true, @@ -2451,7 +2465,7 @@ mod tests { }, allow_net: Some(vec![]), allow_env: true, - allow_run: true, + allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), allow_plugin: true, @@ -2488,7 +2502,7 @@ mod tests { inspect: Some("127.0.0.1:9229".parse().unwrap()), allow_net: Some(vec![]), allow_env: true, - allow_run: true, + allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), allow_plugin: true, @@ -2518,7 +2532,7 @@ mod tests { argv: svec!["arg1", "arg2"], allow_net: Some(vec![]), allow_env: true, - allow_run: true, + allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), allow_plugin: true, @@ -2538,7 +2552,7 @@ mod tests { subcommand: DenoSubcommand::Repl, allow_net: Some(vec![]), allow_env: true, - allow_run: true, + allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), allow_plugin: true, @@ -2572,7 +2586,7 @@ mod tests { inspect: Some("127.0.0.1:9229".parse().unwrap()), allow_net: Some(vec![]), allow_env: true, - allow_run: true, + allow_run: Some(vec![]), allow_read: Some(vec![]), allow_write: Some(vec![]), allow_plugin: true, diff --git a/cli/tests/089_run_allow_list.ts b/cli/tests/089_run_allow_list.ts new file mode 100644 index 0000000000..85c1730a11 --- /dev/null +++ b/cli/tests/089_run_allow_list.ts @@ -0,0 +1,13 @@ +try { + Deno.run({ + cmd: ["ls"], + }); +} catch (e) { + console.log(e); +} + +const proc = Deno.run({ + cmd: ["cat", "089_run_allow_list.ts"], + stdout: "null", +}); +console.log((await proc.status()).success); diff --git a/cli/tests/089_run_allow_list.ts.out b/cli/tests/089_run_allow_list.ts.out new file mode 100644 index 0000000000..68a4a2ac57 --- /dev/null +++ b/cli/tests/089_run_allow_list.ts.out @@ -0,0 +1,3 @@ +[WILDCARD]PermissionDenied: Requires run access to "ls", run again with the --allow-run flag +[WILDCARD] +true diff --git a/cli/tests/090_run_permissions_request.ts b/cli/tests/090_run_permissions_request.ts new file mode 100644 index 0000000000..044bc6e8e2 --- /dev/null +++ b/cli/tests/090_run_permissions_request.ts @@ -0,0 +1,9 @@ +const status1 = + (await Deno.permissions.request({ name: "run", command: "ls" })).state; +const status2 = + (await Deno.permissions.query({ name: "run", command: "cat" })).state; +const status3 = + (await Deno.permissions.request({ name: "run", command: "cat" })).state; +console.log(status1); +console.log(status2); +console.log(status3); diff --git a/cli/tests/090_run_permissions_request.ts.out b/cli/tests/090_run_permissions_request.ts.out new file mode 100644 index 0000000000..362425876a --- /dev/null +++ b/cli/tests/090_run_permissions_request.ts.out @@ -0,0 +1,3 @@ +[WILDCARD]granted +prompt +denied diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 310cb3289d..2d4d8995ea 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -2846,6 +2846,21 @@ console.log("finish"); output: "088_dynamic_import_already_evaluating.ts.out", }); + itest!(_089_run_allow_list { + args: "run --allow-run=cat 089_run_allow_list.ts", + output: "089_run_allow_list.ts.out", + }); + + #[cfg(unix)] + #[test] + fn _090_run_permissions_request() { + let args = "run 090_run_permissions_request.ts"; + let output = "090_run_permissions_request.ts.out"; + let input = b"g\nd\n"; + + util::test_pty(args, output, input); + } + itest!(js_import_detect { args: "run --quiet --reload js_import_detect.ts", output: "js_import_detect.ts.out", diff --git a/runtime/ops/permissions.rs b/runtime/ops/permissions.rs index be8c9974cc..77d095d84e 100644 --- a/runtime/ops/permissions.rs +++ b/runtime/ops/permissions.rs @@ -21,6 +21,7 @@ pub struct PermissionArgs { name: String, path: Option, host: Option, + command: Option, } pub fn op_query_permission( @@ -41,7 +42,7 @@ pub fn op_query_permission( .as_ref(), ), "env" => permissions.env.query(), - "run" => permissions.run.query(), + "run" => permissions.run.query(args.command.as_deref()), "plugin" => permissions.plugin.query(), "hrtime" => permissions.hrtime.query(), n => { @@ -72,7 +73,7 @@ pub fn op_revoke_permission( .as_ref(), ), "env" => permissions.env.revoke(), - "run" => permissions.run.revoke(), + "run" => permissions.run.revoke(args.command.as_deref()), "plugin" => permissions.plugin.revoke(), "hrtime" => permissions.hrtime.revoke(), n => { @@ -103,7 +104,7 @@ pub fn op_request_permission( .as_ref(), ), "env" => permissions.env.request(), - "run" => permissions.run.request(), + "run" => permissions.run.request(args.command.as_deref()), "plugin" => permissions.plugin.request(), "hrtime" => permissions.hrtime.request(), n => { diff --git a/runtime/ops/process.rs b/runtime/ops/process.rs index c2ca2c6872..625bc204ce 100644 --- a/runtime/ops/process.rs +++ b/runtime/ops/process.rs @@ -96,9 +96,8 @@ fn op_run( run_args: RunArgs, _zero_copy: Option, ) -> Result { - state.borrow::().run.check()?; - let args = run_args.cmd; + state.borrow::().run.check(&args[0])?; let env = run_args.env; let cwd = run_args.cwd; @@ -198,11 +197,6 @@ async fn op_run_status( rid: ResourceId, _zero_copy: Option, ) -> Result { - { - let s = state.borrow(); - s.borrow::().run.check()?; - } - let resource = state .borrow_mut() .resource_table @@ -292,7 +286,7 @@ fn op_kill( _zero_copy: Option, ) -> Result<(), AnyError> { super::check_unstable(state, "Deno.kill"); - state.borrow::().run.check()?; + state.borrow::().run.check_all()?; kill(args.pid, args.signo)?; Ok(()) diff --git a/runtime/ops/worker_host.rs b/runtime/ops/worker_host.rs index d8e60171e0..2f297fb08f 100644 --- a/runtime/ops/worker_host.rs +++ b/runtime/ops/worker_host.rs @@ -5,6 +5,7 @@ use crate::permissions::NetDescriptor; use crate::permissions::PermissionState; use crate::permissions::Permissions; use crate::permissions::ReadDescriptor; +use crate::permissions::RunDescriptor; use crate::permissions::UnaryPermission; use crate::permissions::UnitPermission; use crate::permissions::WriteDescriptor; @@ -189,6 +190,26 @@ fn merge_write_permission( Ok(main) } +fn merge_run_permission( + mut main: UnaryPermission, + worker: Option>, +) -> Result, AnyError> { + if let Some(worker) = worker { + if (worker.global_state < main.global_state) + || !worker.granted_list.iter().all(|x| main.check(&x.0).is_ok()) + { + return Err(custom_error( + "PermissionDenied", + "Can't escalate parent thread permissions", + )); + } else { + main.global_state = worker.global_state; + main.granted_list = worker.granted_list; + } + } + Ok(main) +} + fn create_worker_permissions( main_perms: Permissions, worker_perms: PermissionsArg, @@ -199,7 +220,7 @@ fn create_worker_permissions( net: merge_net_permission(main_perms.net, worker_perms.net)?, plugin: merge_boolean_permission(main_perms.plugin, worker_perms.plugin)?, read: merge_read_permission(main_perms.read, worker_perms.read)?, - run: merge_boolean_permission(main_perms.run, worker_perms.run)?, + run: merge_run_permission(main_perms.run, worker_perms.run)?, write: merge_write_permission(main_perms.write, worker_perms.write)?, }) } @@ -216,8 +237,8 @@ struct PermissionsArg { plugin: Option, #[serde(default, deserialize_with = "as_unary_read_permission")] read: Option>, - #[serde(default, deserialize_with = "as_permission_state")] - run: Option, + #[serde(default, deserialize_with = "as_unary_run_permission")] + run: Option>, #[serde(default, deserialize_with = "as_unary_write_permission")] write: Option>, } @@ -349,6 +370,22 @@ where })) } +fn as_unary_run_permission<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let value: UnaryPermissionBase = + deserializer.deserialize_any(ParseBooleanOrStringVec)?; + + Ok(Some(UnaryPermission:: { + global_state: value.global_state, + granted_list: value.paths.into_iter().map(RunDescriptor).collect(), + ..Default::default() + })) +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateWorkerArgs { diff --git a/runtime/permissions.rs b/runtime/permissions.rs index 45b7c0bf19..dba6dc1a5c 100644 --- a/runtime/permissions.rs +++ b/runtime/permissions.rs @@ -146,6 +146,9 @@ impl fmt::Display for NetDescriptor { } } +#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Deserialize)] +pub struct RunDescriptor(pub String); + impl UnaryPermission { pub fn query(&self, path: Option<&Path>) -> PermissionState { let path = path.map(|p| resolve_from_cwd(p).unwrap()); @@ -466,13 +469,126 @@ impl UnaryPermission { } } +impl UnaryPermission { + pub fn query(&self, cmd: Option<&str>) -> PermissionState { + if self.global_state == PermissionState::Denied + && match cmd { + None => true, + Some(cmd) => self.denied_list.iter().any(|cmd_| cmd_.0 == cmd), + } + { + PermissionState::Denied + } else if self.global_state == PermissionState::Granted + || match cmd { + None => false, + Some(cmd) => self.granted_list.iter().any(|cmd_| cmd_.0 == cmd), + } + { + PermissionState::Granted + } else { + PermissionState::Prompt + } + } + + pub fn request(&mut self, cmd: Option<&str>) -> PermissionState { + if let Some(cmd) = cmd { + let state = self.query(Some(&cmd)); + if state == PermissionState::Prompt { + if permission_prompt(&format!("run access to \"{}\"", cmd)) { + self.granted_list.retain(|cmd_| cmd_.0 != cmd); + self.granted_list.insert(RunDescriptor(cmd.to_string())); + PermissionState::Granted + } else { + self.denied_list.retain(|cmd_| cmd_.0 != cmd); + self.denied_list.insert(RunDescriptor(cmd.to_string())); + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + } else { + state + } + } else { + let state = self.query(None); + if state == PermissionState::Prompt { + if permission_prompt("run access") { + self.granted_list.clear(); + self.global_state = PermissionState::Granted; + PermissionState::Granted + } else { + self.global_state = PermissionState::Denied; + PermissionState::Denied + } + } else { + state + } + } + } + + pub fn revoke(&mut self, cmd: Option<&str>) -> PermissionState { + if let Some(cmd) = cmd { + self.granted_list.retain(|cmd_| cmd_.0 != cmd); + } else { + self.granted_list.clear(); + if self.global_state == PermissionState::Granted { + self.global_state = PermissionState::Prompt; + } + } + self.query(cmd) + } + + pub fn check(&self, cmd: &str) -> Result<(), AnyError> { + self + .query(Some(cmd)) + .check(self.name, Some(&format!("\"{}\"", cmd))) + } + + pub fn check_all(&self) -> Result<(), AnyError> { + self.query(None).check(self.name, Some("all")) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct BooleanPermission { + pub name: &'static str, + pub description: &'static str, + pub state: PermissionState, +} + +impl BooleanPermission { + pub fn query(&self) -> PermissionState { + self.state + } + + pub fn request(&mut self) -> PermissionState { + if self.state == PermissionState::Prompt { + if permission_prompt(&format!("access to {}", self.description)) { + self.state = PermissionState::Granted; + } else { + self.state = PermissionState::Denied; + } + } + self.state + } + + pub fn revoke(&mut self) -> PermissionState { + if self.state == PermissionState::Granted { + self.state = PermissionState::Prompt; + } + self.state + } + + pub fn check(&self) -> Result<(), AnyError> { + self.state.check(self.name, None) + } +} + #[derive(Clone, Debug, Default, PartialEq)] pub struct Permissions { pub read: UnaryPermission, pub write: UnaryPermission, pub net: UnaryPermission, pub env: UnitPermission, - pub run: UnitPermission, + pub run: UnaryPermission, pub plugin: UnitPermission, pub hrtime: UnitPermission, } @@ -484,7 +600,7 @@ pub struct PermissionsOptions { pub allow_net: Option>, pub allow_plugin: bool, pub allow_read: Option>, - pub allow_run: bool, + pub allow_run: Option>, pub allow_write: Option>, } @@ -536,8 +652,19 @@ impl Permissions { boolean_permission_from_flag_bool(state, "env", "environment variables") } - pub fn new_run(state: bool) -> UnitPermission { - boolean_permission_from_flag_bool(state, "run", "run a subprocess") + pub fn new_run( + state: &Option>, + ) -> UnaryPermission { + UnaryPermission:: { + name: "run", + description: "run a subprocess", + global_state: global_state_from_option(state), + granted_list: state + .as_ref() + .map(|v| v.iter().map(|x| RunDescriptor(x.clone())).collect()) + .unwrap_or_else(HashSet::new), + denied_list: Default::default(), + } } pub fn new_plugin(state: bool) -> UnitPermission { @@ -554,7 +681,7 @@ impl Permissions { write: Permissions::new_write(&opts.allow_write), net: Permissions::new_net(&opts.allow_net), env: Permissions::new_env(opts.allow_env), - run: Permissions::new_run(opts.allow_run), + run: Permissions::new_run(&opts.allow_run), plugin: Permissions::new_plugin(opts.allow_plugin), hrtime: Permissions::new_hrtime(opts.allow_hrtime), } @@ -566,7 +693,7 @@ impl Permissions { write: Permissions::new_write(&Some(vec![])), net: Permissions::new_net(&Some(vec![])), env: Permissions::new_env(true), - run: Permissions::new_run(true), + run: Permissions::new_run(&Some(vec![])), plugin: Permissions::new_plugin(true), hrtime: Permissions::new_hrtime(true), } @@ -1062,9 +1189,9 @@ mod tests { state: PermissionState::Prompt, ..Default::default() }, - run: UnitPermission { - state: PermissionState::Prompt, - ..Default::default() + run: UnaryPermission { + global_state: PermissionState::Prompt, + ..Permissions::new_run(&Some(svec!["deno"])) }, plugin: UnitPermission { state: PermissionState::Prompt, @@ -1093,8 +1220,10 @@ mod tests { assert_eq!(perms2.net.query(Some(&("127.0.0.1", Some(8000)))), PermissionState::Granted); assert_eq!(perms1.env.query(), PermissionState::Granted); assert_eq!(perms2.env.query(), PermissionState::Prompt); - assert_eq!(perms1.run.query(), PermissionState::Granted); - assert_eq!(perms2.run.query(), PermissionState::Prompt); + assert_eq!(perms1.run.query(None), PermissionState::Granted); + assert_eq!(perms1.run.query(Some(&"deno".to_string())), PermissionState::Granted); + assert_eq!(perms2.run.query(None), PermissionState::Prompt); + assert_eq!(perms2.run.query(Some(&"deno".to_string())), PermissionState::Granted); assert_eq!(perms1.plugin.query(), PermissionState::Granted); assert_eq!(perms2.plugin.query(), PermissionState::Prompt); assert_eq!(perms1.hrtime.query(), PermissionState::Granted); @@ -1126,10 +1255,11 @@ mod tests { assert_eq!(perms.env.request(), PermissionState::Granted); set_prompt_result(false); assert_eq!(perms.env.request(), PermissionState::Granted); - set_prompt_result(false); - assert_eq!(perms.run.request(), PermissionState::Denied); set_prompt_result(true); - assert_eq!(perms.run.request(), PermissionState::Denied); + assert_eq!(perms.run.request(Some(&"deno".to_string())), PermissionState::Granted); + assert_eq!(perms.run.query(None), PermissionState::Prompt); + set_prompt_result(false); + assert_eq!(perms.run.request(Some(&"deno".to_string())), PermissionState::Granted); set_prompt_result(true); assert_eq!(perms.plugin.request(), PermissionState::Granted); set_prompt_result(false); @@ -1160,9 +1290,9 @@ mod tests { state: PermissionState::Granted, ..Default::default() }, - run: UnitPermission { - state: PermissionState::Granted, - ..Default::default() + run: UnaryPermission { + global_state: PermissionState::Prompt, + ..Permissions::new_run(&Some(svec!["deno"])) }, plugin: UnitPermission { state: PermissionState::Prompt, @@ -1184,7 +1314,7 @@ mod tests { assert_eq!(perms.net.revoke(Some(&("127.0.0.1", Some(8000)))), PermissionState::Granted); assert_eq!(perms.net.revoke(Some(&("127.0.0.1", None))), PermissionState::Prompt); assert_eq!(perms.env.revoke(), PermissionState::Prompt); - assert_eq!(perms.run.revoke(), PermissionState::Prompt); + assert_eq!(perms.run.revoke(Some(&"deno".to_string())), PermissionState::Prompt); assert_eq!(perms.plugin.revoke(), PermissionState::Prompt); assert_eq!(perms.hrtime.revoke(), PermissionState::Denied); };