From fa9e7aab6d49f241a4eb30cc0e261f8ceb64af2f Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Wed, 28 Sep 2022 21:46:50 +0900 Subject: [PATCH] feat: add --allow-sys permission flag (#16028) --- cli/args/flags.rs | 130 ++++++++++++ cli/dts/lib.deno.ns.d.ts | 24 +++ cli/dts/lib.deno.unstable.d.ts | 28 +-- cli/tests/unit/network_interfaces_test.ts | 5 +- cli/tests/unit/os_test.ts | 62 +++--- cli/tests/unit/permissions_test.ts | 17 ++ cli/tools/standalone.rs | 1 + runtime/js/10_permissions.js | 7 +- runtime/ops/os.rs | 45 +++- runtime/ops/permissions.rs | 21 ++ runtime/permissions.rs | 238 ++++++++++++++++++++++ 11 files changed, 527 insertions(+), 51 deletions(-) diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 513307e929..46fa8c5520 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -291,6 +291,7 @@ pub struct Flags { pub allow_ffi: Option>, pub allow_read: Option>, pub allow_run: Option>, + pub allow_sys: Option>, pub allow_write: Option>, pub ca_stores: Option>, pub ca_file: Option, @@ -413,6 +414,17 @@ impl Flags { _ => {} } + match &self.allow_sys { + Some(sys_allowlist) if sys_allowlist.is_empty() => { + args.push("--allow-sys".to_string()); + } + Some(sys_allowlist) => { + let s = format!("--allow-sys={}", sys_allowlist.join(",")); + args.push(s) + } + _ => {} + } + match &self.allow_ffi { Some(ffi_allowlist) if ffi_allowlist.is_empty() => { args.push("--allow-ffi".to_string()); @@ -470,6 +482,7 @@ impl Flags { allow_ffi: self.allow_ffi.clone(), allow_read: self.allow_read.clone(), allow_run: self.allow_run.clone(), + allow_sys: self.allow_sys.clone(), allow_write: self.allow_write.clone(), prompt: !self.no_prompt, } @@ -590,6 +603,7 @@ fn handle_repl_flags(flags: &mut Flags, repl_flags: ReplFlags) { flags.allow_env = Some(vec![]); flags.allow_run = Some(vec![]); flags.allow_read = Some(vec![]); + flags.allow_sys = Some(vec![]); flags.allow_write = Some(vec![]); flags.allow_ffi = Some(vec![]); flags.allow_hrtime = true; @@ -1810,6 +1824,27 @@ fn permission_args(app: Command) -> Command { Ok(()) }), ) + .arg( + Arg::new("allow-sys") + .long("allow-sys") + .min_values(0) + .takes_value(true) + .use_value_delimiter(true) + .require_equals(true) + .help("Allow access to system info") + .validator(|keys| { + for key in keys.split(',') { + match key { + "hostname" | "osRelease" | "loadavg" | "networkInterfaces" + | "systemMemoryInfo" | "getUid" | "getGid" => {} + _ => { + return Err(format!("unknown system info kind \"{}\"", key)); + } + } + } + Ok(()) + }), + ) .arg( Arg::new("allow-run") .long("allow-run") @@ -2367,6 +2402,7 @@ fn eval_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.allow_env = Some(vec![]); flags.allow_run = Some(vec![]); flags.allow_read = Some(vec![]); + flags.allow_sys = Some(vec![]); flags.allow_write = Some(vec![]); flags.allow_ffi = Some(vec![]); flags.allow_hrtime = true; @@ -2870,6 +2906,12 @@ fn permission_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) { debug!("run allowlist: {:#?}", &flags.allow_run); } + if let Some(sys_wl) = matches.values_of("allow-sys") { + let sys_allowlist: Vec = sys_wl.map(ToString::to_string).collect(); + flags.allow_sys = Some(sys_allowlist); + debug!("sys info allowlist: {:#?}", &flags.allow_sys); + } + if let Some(ffi_wl) = matches.values_of("allow-ffi") { let ffi_allowlist: Vec = ffi_wl.map(PathBuf::from).collect(); flags.allow_ffi = Some(ffi_allowlist); @@ -2886,6 +2928,7 @@ fn permission_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.allow_net = Some(vec![]); flags.allow_run = Some(vec![]); flags.allow_write = Some(vec![]); + flags.allow_sys = Some(vec![]); flags.allow_ffi = Some(vec![]); flags.allow_hrtime = true; } @@ -3351,6 +3394,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -3978,6 +4022,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -4001,6 +4046,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -4025,6 +4071,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -4062,6 +4109,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -4092,6 +4140,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -4115,6 +4164,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -4151,6 +4201,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -4175,6 +4226,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -4203,6 +4255,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -4329,6 +4382,81 @@ mod tests { assert!(r.is_err()); } + #[test] + fn allow_sys() { + let r = flags_from_vec(svec!["deno", "run", "--allow-sys", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "script.ts".to_string(), + }), + allow_sys: Some(vec![]), + ..Flags::default() + } + ); + } + + #[test] + fn allow_sys_allowlist() { + let r = + flags_from_vec(svec!["deno", "run", "--allow-sys=hostname", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "script.ts".to_string(), + }), + allow_sys: Some(svec!["hostname"]), + ..Flags::default() + } + ); + } + + #[test] + fn allow_sys_allowlist_multiple() { + let r = flags_from_vec(svec![ + "deno", + "run", + "--allow-sys=hostname,osRelease", + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "script.ts".to_string(), + }), + allow_sys: Some(svec!["hostname", "osRelease"]), + ..Flags::default() + } + ); + } + + #[test] + fn allow_sys_allowlist_validator() { + let r = + flags_from_vec(svec!["deno", "run", "--allow-sys=hostname", "script.ts"]); + assert!(r.is_ok()); + let r = flags_from_vec(svec![ + "deno", + "run", + "--allow-sys=hostname,osRelease", + "script.ts" + ]); + assert!(r.is_ok()); + let r = + flags_from_vec(svec!["deno", "run", "--allow-sys=foo", "script.ts"]); + assert!(r.is_err()); + let r = flags_from_vec(svec![ + "deno", + "run", + "--allow-sys=hostname,foo", + "script.ts" + ]); + assert!(r.is_err()); + } + #[test] fn reload_validator() { let r = flags_from_vec(svec![ @@ -4931,6 +5059,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, @@ -5012,6 +5141,7 @@ mod tests { allow_env: Some(vec![]), allow_run: Some(vec![]), allow_read: Some(vec![]), + allow_sys: Some(vec![]), allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, diff --git a/cli/dts/lib.deno.ns.d.ts b/cli/dts/lib.deno.ns.d.ts index 97d3fed045..095ae139ab 100644 --- a/cli/dts/lib.deno.ns.d.ts +++ b/cli/dts/lib.deno.ns.d.ts @@ -183,6 +183,15 @@ declare namespace Deno { */ env?: "inherit" | boolean | string[]; + /** Specifies if the `sys` permission should be requested or revoked. + * If set to `"inherit"`, the current `sys` permission will be inherited. + * If set to `true`, the global `sys` permission will be requested. + * If set to `false`, the global `sys` permission will be revoked. + * + * Defaults to `false`. + */ + sys?: "inherit" | boolean | string[]; + /** Specifies if the `hrtime` permission should be requested or revoked. * If set to `"inherit"`, the current `hrtime` permission will be inherited. * If set to `true`, the global `hrtime` permission will be requested. @@ -2913,6 +2922,7 @@ declare namespace Deno { | "write" | "net" | "env" + | "sys" | "ffi" | "hrtime"; @@ -2957,6 +2967,19 @@ declare namespace Deno { variable?: string; } + /** @category Permissions */ + export interface SysPermissionDescriptor { + name: "sys"; + kind?: + | "loadavg" + | "hostname" + | "systemMemoryInfo" + | "networkInterfaces" + | "osRelease" + | "getUid" + | "getGid"; + } + /** @category Permissions */ export interface FfiPermissionDescriptor { name: "ffi"; @@ -2979,6 +3002,7 @@ declare namespace Deno { | WritePermissionDescriptor | NetPermissionDescriptor | EnvPermissionDescriptor + | SysPermissionDescriptor | FfiPermissionDescriptor | HrtimePermissionDescriptor; diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index ec87fa9ca5..6cb062396b 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -255,11 +255,11 @@ declare namespace Deno { * console.log(Deno.loadavg()); // e.g. [ 0.71, 0.44, 0.44 ] * ``` * - * Requires `allow-env` permission. + * Requires `allow-sys` permission. * There are questions around which permission this needs. And maybe should be * renamed (loadAverage?). * - * @tags allow-env + * @tags allow-sys * @category Observability */ export function loadavg(): number[]; @@ -272,11 +272,11 @@ declare namespace Deno { * console.log(Deno.osRelease()); * ``` * - * Requires `allow-env` permission. + * Requires `allow-sys` permission. * Under consideration to possibly move to Deno.build or Deno.versions and if * it should depend sys-info, which may not be desirable. * - * @tags allow-env + * @tags allow-sys * @category Runtime Environment */ export function osRelease(): string; @@ -292,9 +292,9 @@ declare namespace Deno { * console.log(Deno.systemMemoryInfo()); * ``` * - * Requires `allow-env` permission. + * Requires `allow-sys` permission. * - * @tags allow-env + * @tags allow-sys * @category Runtime Environment */ export function systemMemoryInfo(): SystemMemoryInfo; @@ -355,9 +355,9 @@ declare namespace Deno { * console.log(Deno.networkInterfaces()); * ``` * - * Requires `allow-env` permission. + * Requires `allow-sys` permission. * - * @tags allow-env + * @tags allow-sys * @category Network */ export function networkInterfaces(): NetworkInterfaceInfo[]; @@ -370,9 +370,9 @@ declare namespace Deno { * console.log(Deno.getUid()); * ``` * - * Requires `allow-env` permission. + * Requires `allow-sys` permission. * - * @tags allow-env + * @tags allow-sys * @category Runtime Environment */ export function getUid(): number | null; @@ -385,9 +385,9 @@ declare namespace Deno { * console.log(Deno.getGid()); * ``` * - * Requires `allow-env` permission. + * Requires `allow-sys` permission. * - * @tags allow-env + * @tags allow-sys * @category Runtime Environment */ export function getGid(): number | null; @@ -980,11 +980,11 @@ declare namespace Deno { * console.log(Deno.hostname()); * ``` * - * Requires `allow-env` permission. + * Requires `allow-sys` permission. * Additional consideration is still necessary around the permissions * required. * - * @tags allow-env + * @tags allow-sys * @category Runtime Environment */ export function hostname(): string; diff --git a/cli/tests/unit/network_interfaces_test.ts b/cli/tests/unit/network_interfaces_test.ts index 120f587639..a0e6e691a1 100644 --- a/cli/tests/unit/network_interfaces_test.ts +++ b/cli/tests/unit/network_interfaces_test.ts @@ -1,7 +1,10 @@ import { assert } from "./test_util.ts"; Deno.test( - { name: "Deno.networkInterfaces", permissions: { env: true } }, + { + name: "Deno.networkInterfaces", + permissions: { sys: ["networkInterfaces"] }, + }, () => { const networkInterfaces = Deno.networkInterfaces(); assert(Array.isArray(networkInterfaces)); diff --git a/cli/tests/unit/os_test.ts b/cli/tests/unit/os_test.ts index bdbf7f0ca5..d620ae4e35 100644 --- a/cli/tests/unit/os_test.ts +++ b/cli/tests/unit/os_test.ts @@ -187,49 +187,61 @@ Deno.test({ permissions: { read: false } }, function execPathPerm() { ); }); -Deno.test({ permissions: { env: true } }, function loadavgSuccess() { - const load = Deno.loadavg(); - assertEquals(load.length, 3); -}); +Deno.test( + { permissions: { sys: ["loadavg"] } }, + function loadavgSuccess() { + const load = Deno.loadavg(); + assertEquals(load.length, 3); + }, +); -Deno.test({ permissions: { env: false } }, function loadavgPerm() { +Deno.test({ permissions: { sys: false } }, function loadavgPerm() { assertThrows(() => { Deno.loadavg(); }, Deno.errors.PermissionDenied); }); -Deno.test({ permissions: { env: true } }, function hostnameDir() { - assertNotEquals(Deno.hostname(), ""); -}); +Deno.test( + { permissions: { sys: ["hostname"] } }, + function hostnameDir() { + assertNotEquals(Deno.hostname(), ""); + }, +); -Deno.test({ permissions: { env: false } }, function hostnamePerm() { +Deno.test({ permissions: { sys: false } }, function hostnamePerm() { assertThrows(() => { Deno.hostname(); }, Deno.errors.PermissionDenied); }); -Deno.test({ permissions: { env: true } }, function releaseDir() { - assertNotEquals(Deno.osRelease(), ""); -}); +Deno.test( + { permissions: { sys: ["osRelease"] } }, + function releaseDir() { + assertNotEquals(Deno.osRelease(), ""); + }, +); -Deno.test({ permissions: { env: false } }, function releasePerm() { +Deno.test({ permissions: { sys: false } }, function releasePerm() { assertThrows(() => { Deno.osRelease(); }, Deno.errors.PermissionDenied); }); -Deno.test({ permissions: { env: true } }, function systemMemoryInfo() { - const info = Deno.systemMemoryInfo(); - assert(info.total >= 0); - assert(info.free >= 0); - assert(info.available >= 0); - assert(info.buffers >= 0); - assert(info.cached >= 0); - assert(info.swapTotal >= 0); - assert(info.swapFree >= 0); -}); +Deno.test( + { permissions: { sys: ["systemMemoryInfo"] } }, + function systemMemoryInfo() { + const info = Deno.systemMemoryInfo(); + assert(info.total >= 0); + assert(info.free >= 0); + assert(info.available >= 0); + assert(info.buffers >= 0); + assert(info.cached >= 0); + assert(info.swapTotal >= 0); + assert(info.swapFree >= 0); + }, +); -Deno.test({ permissions: { env: true } }, function getUid() { +Deno.test({ permissions: { sys: ["getUid"] } }, function getUid() { if (Deno.build.os === "windows") { assertEquals(Deno.getUid(), null); } else { @@ -239,7 +251,7 @@ Deno.test({ permissions: { env: true } }, function getUid() { } }); -Deno.test({ permissions: { env: true } }, function getGid() { +Deno.test({ permissions: { sys: ["getGid"] } }, function getGid() { if (Deno.build.os === "windows") { assertEquals(Deno.getGid(), null); } else { diff --git a/cli/tests/unit/permissions_test.ts b/cli/tests/unit/permissions_test.ts index 551e9bdcd8..c0945bb598 100644 --- a/cli/tests/unit/permissions_test.ts +++ b/cli/tests/unit/permissions_test.ts @@ -19,6 +19,23 @@ Deno.test(async function permissionNetInvalidHost() { }, URIError); }); +Deno.test(async function permissionSysValidKind() { + await Deno.permissions.query({ name: "sys", kind: "loadavg" }); + await Deno.permissions.query({ name: "sys", kind: "osRelease" }); + await Deno.permissions.query({ name: "sys", kind: "networkInterfaces" }); + await Deno.permissions.query({ name: "sys", kind: "systemMemoryInfo" }); + await Deno.permissions.query({ name: "sys", kind: "hostname" }); + await Deno.permissions.query({ name: "sys", kind: "getUid" }); + await Deno.permissions.query({ name: "sys", kind: "getGid" }); +}); + +Deno.test(async function permissionSysInvalidKind() { + await assertRejects(async () => { + // deno-lint-ignore no-explicit-any + await Deno.permissions.query({ name: "sys", kind: "abc" as any }); + }, TypeError); +}); + Deno.test(async function permissionQueryReturnsEventTarget() { const status = await Deno.permissions.query({ name: "hrtime" }); assert(["granted", "denied", "prompt"].includes(status.state)); diff --git a/cli/tools/standalone.rs b/cli/tools/standalone.rs index 03118f4b68..eeedf8bd5f 100644 --- a/cli/tools/standalone.rs +++ b/cli/tools/standalone.rs @@ -257,6 +257,7 @@ pub fn compile_to_runtime_flags( allow_ffi: flags.allow_ffi.clone(), allow_read: flags.allow_read.clone(), allow_run: flags.allow_run.clone(), + allow_sys: flags.allow_sys.clone(), allow_write: flags.allow_write.clone(), ca_stores: flags.ca_stores.clone(), ca_file: flags.ca_file.clone(), diff --git a/runtime/js/10_permissions.js b/runtime/js/10_permissions.js index 2573816dfb..6aad0677d4 100644 --- a/runtime/js/10_permissions.js +++ b/runtime/js/10_permissions.js @@ -32,12 +32,13 @@ * @property {PermissionStatus} status */ - /** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "run" | "ffi" | "hrtime">} */ + /** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "sys" | "run" | "ffi" | "hrtime">} */ const permissionNames = [ "read", "write", "net", "env", + "sys", "run", "ffi", "hrtime", @@ -132,6 +133,8 @@ key += `-${desc.command}&`; } else if (desc.name === "env" && desc.variable) { key += `-${desc.variable}&`; + } else if (desc.name === "sys" && desc.kind) { + key += `-${desc.kind}&`; } else { key += "$"; } @@ -242,7 +245,7 @@ serializedPermissions[key] = permissions[key]; } } - for (const key of ["env", "hrtime", "net"]) { + for (const key of ["env", "hrtime", "net", "sys"]) { if (ArrayIsArray(permissions[key])) { serializedPermissions[key] = ArrayPrototypeSlice(permissions[key]); } else { diff --git a/runtime/ops/os.rs b/runtime/ops/os.rs index 14c4229a17..0e51e31208 100644 --- a/runtime/ops/os.rs +++ b/runtime/ops/os.rs @@ -161,7 +161,10 @@ fn op_exit(state: &mut OpState) { #[op] fn op_loadavg(state: &mut OpState) -> Result<(f64, f64, f64), AnyError> { super::check_unstable(state, "Deno.loadavg"); - state.borrow_mut::().env.check_all()?; + state + .borrow_mut::() + .sys + .check("loadavg", Some("Deno.loadavg()"))?; match sys_info::loadavg() { Ok(loadavg) => Ok((loadavg.one, loadavg.five, loadavg.fifteen)), Err(_) => Ok((0.0, 0.0, 0.0)), @@ -171,7 +174,10 @@ fn op_loadavg(state: &mut OpState) -> Result<(f64, f64, f64), AnyError> { #[op] fn op_hostname(state: &mut OpState) -> Result { super::check_unstable(state, "Deno.hostname"); - state.borrow_mut::().env.check_all()?; + state + .borrow_mut::() + .sys + .check("hostname", Some("Deno.hostname()"))?; let hostname = sys_info::hostname().unwrap_or_else(|_| "".to_string()); Ok(hostname) } @@ -179,7 +185,10 @@ fn op_hostname(state: &mut OpState) -> Result { #[op] fn op_os_release(state: &mut OpState) -> Result { super::check_unstable(state, "Deno.osRelease"); - state.borrow_mut::().env.check_all()?; + state + .borrow_mut::() + .sys + .check("osRelease", Some("Deno.osRelease()"))?; let release = sys_info::os_release().unwrap_or_else(|_| "".to_string()); Ok(release) } @@ -189,7 +198,10 @@ fn op_network_interfaces( state: &mut OpState, ) -> Result, AnyError> { super::check_unstable(state, "Deno.networkInterfaces"); - state.borrow_mut::().env.check_all()?; + state + .borrow_mut::() + .sys + .check("networkInterfaces", Some("Deno.networkInterfaces()"))?; Ok(netif::up()?.map(NetworkInterface::from).collect()) } @@ -255,7 +267,10 @@ fn op_system_memory_info( state: &mut OpState, ) -> Result, AnyError> { super::check_unstable(state, "Deno.systemMemoryInfo"); - state.borrow_mut::().env.check_all()?; + state + .borrow_mut::() + .sys + .check("systemMemoryInfo", Some("Deno.systemMemoryInfo()"))?; match sys_info::mem_info() { Ok(info) => Ok(Some(MemInfo { total: info.total, @@ -274,7 +289,10 @@ fn op_system_memory_info( #[op] fn op_getgid(state: &mut OpState) -> Result, AnyError> { super::check_unstable(state, "Deno.getGid"); - state.borrow_mut::().env.check_all()?; + state + .borrow_mut::() + .sys + .check("getGid", Some("Deno.getGid()"))?; // TODO(bartlomieju): #[allow(clippy::undocumented_unsafe_blocks)] unsafe { @@ -286,7 +304,10 @@ fn op_getgid(state: &mut OpState) -> Result, AnyError> { #[op] fn op_getgid(state: &mut OpState) -> Result, AnyError> { super::check_unstable(state, "Deno.getGid"); - state.borrow_mut::().env.check_all()?; + state + .borrow_mut::() + .sys + .check("getGid", Some("Deno.getGid()"))?; Ok(None) } @@ -294,7 +315,10 @@ fn op_getgid(state: &mut OpState) -> Result, AnyError> { #[op] fn op_getuid(state: &mut OpState) -> Result, AnyError> { super::check_unstable(state, "Deno.getUid"); - state.borrow_mut::().env.check_all()?; + state + .borrow_mut::() + .sys + .check("getUid", Some("Deno.getUid()"))?; // TODO(bartlomieju): #[allow(clippy::undocumented_unsafe_blocks)] unsafe { @@ -306,6 +330,9 @@ fn op_getuid(state: &mut OpState) -> Result, AnyError> { #[op] fn op_getuid(state: &mut OpState) -> Result, AnyError> { super::check_unstable(state, "Deno.getUid"); - state.borrow_mut::().env.check_all()?; + state + .borrow_mut::() + .sys + .check("getUid", Some("Deno.getUid()"))?; Ok(None) } diff --git a/runtime/ops/permissions.rs b/runtime/ops/permissions.rs index ed17977012..b79330f7ff 100644 --- a/runtime/ops/permissions.rs +++ b/runtime/ops/permissions.rs @@ -2,6 +2,7 @@ use crate::permissions::Permissions; use deno_core::error::custom_error; +use deno_core::error::type_error; use deno_core::error::uri_error; use deno_core::error::AnyError; use deno_core::op; @@ -27,6 +28,7 @@ pub struct PermissionArgs { path: Option, host: Option, variable: Option, + kind: Option, command: Option, } @@ -48,6 +50,7 @@ pub fn op_query_permission( .as_ref(), ), "env" => permissions.env.query(args.variable.as_deref()), + "sys" => permissions.sys.query(parse_sys_kind(args.kind.as_deref())?), "run" => permissions.run.query(args.command.as_deref()), "ffi" => permissions.ffi.query(args.path.as_deref().map(Path::new)), "hrtime" => permissions.hrtime.query(), @@ -79,6 +82,9 @@ pub fn op_revoke_permission( .as_ref(), ), "env" => permissions.env.revoke(args.variable.as_deref()), + "sys" => permissions + .sys + .revoke(parse_sys_kind(args.kind.as_deref())?), "run" => permissions.run.revoke(args.command.as_deref()), "ffi" => permissions.ffi.revoke(args.path.as_deref().map(Path::new)), "hrtime" => permissions.hrtime.revoke(), @@ -110,6 +116,9 @@ pub fn op_request_permission( .as_ref(), ), "env" => permissions.env.request(args.variable.as_deref()), + "sys" => permissions + .sys + .request(parse_sys_kind(args.kind.as_deref())?), "run" => permissions.run.request(args.command.as_deref()), "ffi" => permissions.ffi.request(args.path.as_deref().map(Path::new)), "hrtime" => permissions.hrtime.request(), @@ -132,3 +141,15 @@ fn parse_host(host_str: &str) -> Result<(String, Option), AnyError> { let hostname = url.host_str().unwrap(); Ok((hostname.to_string(), url.port())) } + +fn parse_sys_kind(kind: Option<&str>) -> Result, AnyError> { + if let Some(kind) = kind { + match kind { + "hostname" | "osRelease" | "loadavg" | "networkInterfaces" + | "systemMemoryInfo" | "getUid" | "getGid" => Ok(Some(kind)), + _ => Err(type_error(format!("unknown system info kind \"{}\"", kind))), + } + } else { + Ok(kind) + } +} diff --git a/runtime/permissions.rs b/runtime/permissions.rs index a502959102..84ff286b4a 100644 --- a/runtime/permissions.rs +++ b/runtime/permissions.rs @@ -301,6 +301,9 @@ impl ToString for RunDescriptor { } } +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct SysDescriptor(pub String); + #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct FfiDescriptor(pub PathBuf); @@ -928,6 +931,128 @@ impl Default for UnaryPermission { } } +impl UnaryPermission { + pub fn query(&self, kind: Option<&str>) -> PermissionState { + if self.global_state == PermissionState::Denied + && match kind { + None => true, + Some(kind) => { + self.denied_list.contains(&SysDescriptor(kind.to_string())) + } + } + { + PermissionState::Denied + } else if self.global_state == PermissionState::Granted + || match kind { + None => false, + Some(kind) => { + self.granted_list.contains(&SysDescriptor(kind.to_string())) + } + } + { + PermissionState::Granted + } else { + PermissionState::Prompt + } + } + + pub fn request(&mut self, kind: Option<&str>) -> PermissionState { + let state = self.query(kind); + if state != PermissionState::Prompt { + return state; + } + if let Some(kind) = kind { + let desc = SysDescriptor(kind.to_string()); + if 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 + } + } else { + if permission_prompt( + "sys access", + self.name, + Some("Deno.permissions.query()"), + ) { + self.global_state = PermissionState::Granted; + } else { + self.granted_list.clear(); + self.global_state = PermissionState::Denied; + } + self.global_state + } + } + + pub fn revoke(&mut self, kind: Option<&str>) -> PermissionState { + if let Some(kind) = kind { + self.granted_list.remove(&SysDescriptor(kind.to_string())); + } else { + self.granted_list.clear(); + } + if self.global_state == PermissionState::Granted { + self.global_state = PermissionState::Prompt; + } + self.query(kind) + } + + pub fn check( + &mut self, + kind: &str, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + let (result, prompted) = self.query(Some(kind)).check( + self.name, + api_name, + Some(&format!("\"{}\"", kind)), + self.prompt, + ); + if prompted { + if result.is_ok() { + self.granted_list.insert(SysDescriptor(kind.to_string())); + } else { + self.denied_list.insert(SysDescriptor(kind.to_string())); + self.global_state = PermissionState::Denied; + } + } + result + } + + pub fn check_all(&mut self) -> Result<(), AnyError> { + let (result, prompted) = + self + .query(None) + .check(self.name, None, Some("all"), self.prompt); + if prompted { + if result.is_ok() { + self.global_state = PermissionState::Granted; + } else { + self.global_state = PermissionState::Denied; + } + } + result + } +} + +impl Default for UnaryPermission { + fn default() -> Self { + UnaryPermission:: { + name: "sys", + description: "system information", + global_state: Default::default(), + granted_list: Default::default(), + denied_list: Default::default(), + prompt: false, + } + } +} + impl UnaryPermission { pub fn query(&self, cmd: Option<&str>) -> PermissionState { if self.global_state == PermissionState::Denied @@ -1221,6 +1346,7 @@ pub struct Permissions { pub write: UnaryPermission, pub net: UnaryPermission, pub env: UnaryPermission, + pub sys: UnaryPermission, pub run: UnaryPermission, pub ffi: UnaryPermission, pub hrtime: UnitPermission, @@ -1233,6 +1359,7 @@ impl Default for Permissions { write: Permissions::new_write(&None, false).unwrap(), net: Permissions::new_net(&None, false).unwrap(), env: Permissions::new_env(&None, false).unwrap(), + sys: Permissions::new_sys(&None, false).unwrap(), run: Permissions::new_run(&None, false).unwrap(), ffi: Permissions::new_ffi(&None, false).unwrap(), hrtime: Permissions::new_hrtime(false), @@ -1248,6 +1375,7 @@ pub struct PermissionsOptions { pub allow_ffi: Option>, pub allow_read: Option>, pub allow_run: Option>, + pub allow_sys: Option>, pub allow_write: Option>, pub prompt: bool, } @@ -1321,6 +1449,31 @@ impl Permissions { }) } + pub fn new_sys( + state: &Option>, + prompt: bool, + ) -> Result, AnyError> { + Ok(UnaryPermission:: { + global_state: global_state_from_option(state), + granted_list: state.as_ref().map_or_else( + || Ok(HashSet::new()), + |v| { + v.iter() + .map(|x| { + if x.is_empty() { + Err(AnyError::msg("emtpy")) + } else { + Ok(SysDescriptor(x.to_string())) + } + }) + .collect() + }, + )?, + prompt, + ..Default::default() + }) + } + pub fn new_run( state: &Option>, prompt: bool, @@ -1373,6 +1526,7 @@ impl Permissions { write: Permissions::new_write(&opts.allow_write, opts.prompt)?, net: Permissions::new_net(&opts.allow_net, opts.prompt)?, env: Permissions::new_env(&opts.allow_env, opts.prompt)?, + sys: Permissions::new_sys(&opts.allow_sys, opts.prompt)?, run: Permissions::new_run(&opts.allow_run, opts.prompt)?, ffi: Permissions::new_ffi(&opts.allow_ffi, opts.prompt)?, hrtime: Permissions::new_hrtime(opts.allow_hrtime), @@ -1385,6 +1539,7 @@ impl Permissions { write: Permissions::new_write(&Some(vec![]), false).unwrap(), net: Permissions::new_net(&Some(vec![]), false).unwrap(), env: Permissions::new_env(&Some(vec![]), false).unwrap(), + sys: Permissions::new_sys(&Some(vec![]), false).unwrap(), run: Permissions::new_run(&Some(vec![]), false).unwrap(), ffi: Permissions::new_ffi(&Some(vec![]), false).unwrap(), hrtime: Permissions::new_hrtime(true), @@ -1722,6 +1877,7 @@ pub struct ChildPermissionsArg { ffi: ChildUnaryPermissionArg, read: ChildUnaryPermissionArg, run: ChildUnaryPermissionArg, + sys: ChildUnaryPermissionArg, write: ChildUnaryPermissionArg, } @@ -1734,6 +1890,7 @@ impl ChildPermissionsArg { ffi: ChildUnaryPermissionArg::Inherit, read: ChildUnaryPermissionArg::Inherit, run: ChildUnaryPermissionArg::Inherit, + sys: ChildUnaryPermissionArg::Inherit, write: ChildUnaryPermissionArg::Inherit, } } @@ -1746,6 +1903,7 @@ impl ChildPermissionsArg { ffi: ChildUnaryPermissionArg::NotGranted, read: ChildUnaryPermissionArg::NotGranted, run: ChildUnaryPermissionArg::NotGranted, + sys: ChildUnaryPermissionArg::NotGranted, write: ChildUnaryPermissionArg::NotGranted, } } @@ -1822,6 +1980,11 @@ impl<'de> Deserialize<'de> for ChildPermissionsArg { child_permissions_arg.run = arg.map_err(|e| { de::Error::custom(format!("(deno.permissions.run) {}", e)) })?; + } else if key == "sys" { + let arg = serde_json::from_value::(value); + child_permissions_arg.sys = arg.map_err(|e| { + de::Error::custom(format!("(deno.permissions.sys) {}", e)) + })?; } else if key == "write" { let arg = serde_json::from_value::(value); child_permissions_arg.write = arg.map_err(|e| { @@ -1872,6 +2035,35 @@ pub fn create_child_permissions( worker_perms.env.global_state = PermissionState::Denied; } worker_perms.env.prompt = main_perms.env.prompt; + match child_permissions_arg.sys { + ChildUnaryPermissionArg::Inherit => { + worker_perms.sys = main_perms.sys.clone(); + } + ChildUnaryPermissionArg::Granted => { + if main_perms.sys.check_all().is_err() { + return Err(escalation_error()); + } + worker_perms.sys.global_state = PermissionState::Granted; + } + ChildUnaryPermissionArg::NotGranted => {} + ChildUnaryPermissionArg::GrantedList(granted_list) => { + worker_perms.sys.granted_list = + Permissions::new_sys(&Some(granted_list), false)?.granted_list; + if !worker_perms + .sys + .granted_list + .iter() + .all(|desc| main_perms.sys.check(&desc.0, None).is_ok()) + { + return Err(escalation_error()); + } + } + } + worker_perms.sys.denied_list = main_perms.sys.denied_list.clone(); + if main_perms.sys.global_state == PermissionState::Denied { + worker_perms.sys.global_state = PermissionState::Denied; + } + worker_perms.sys.prompt = main_perms.sys.prompt; match child_permissions_arg.hrtime { ChildUnitPermissionArg::Inherit => { worker_perms.hrtime = main_perms.hrtime.clone(); @@ -2608,6 +2800,10 @@ mod tests { global_state: PermissionState::Prompt, ..Permissions::new_env(&Some(svec!["HOME"]), false).unwrap() }, + sys: UnaryPermission { + global_state: PermissionState::Prompt, + ..Permissions::new_sys(&Some(svec!["hostname"]), false).unwrap() + }, run: UnaryPermission { global_state: PermissionState::Prompt, ..Permissions::new_run(&Some(svec!["deno"]), false).unwrap() @@ -2642,6 +2838,10 @@ mod tests { assert_eq!(perms1.env.query(Some("HOME")), PermissionState::Granted); assert_eq!(perms2.env.query(None), PermissionState::Prompt); assert_eq!(perms2.env.query(Some("HOME")), PermissionState::Granted); + assert_eq!(perms1.sys.query(None), PermissionState::Granted); + assert_eq!(perms1.sys.query(Some("HOME")), PermissionState::Granted); + assert_eq!(perms2.env.query(None), PermissionState::Prompt); + assert_eq!(perms2.sys.query(Some("hostname")), PermissionState::Granted); assert_eq!(perms1.run.query(None), PermissionState::Granted); assert_eq!(perms1.run.query(Some("deno")), PermissionState::Granted); assert_eq!(perms2.run.query(None), PermissionState::Prompt); @@ -2681,6 +2881,11 @@ mod tests { prompt_value.set(false); assert_eq!(perms.env.request(Some("HOME")), PermissionState::Granted); prompt_value.set(true); + assert_eq!(perms.sys.request(Some("hostname")), PermissionState::Granted); + assert_eq!(perms.sys.query(None), PermissionState::Prompt); + prompt_value.set(false); + assert_eq!(perms.sys.request(Some("hostname")), PermissionState::Granted); + prompt_value.set(true); assert_eq!(perms.run.request(Some("deno")), PermissionState::Granted); assert_eq!(perms.run.query(None), PermissionState::Prompt); prompt_value.set(false); @@ -2728,6 +2933,10 @@ mod tests { global_state: PermissionState::Prompt, ..Permissions::new_env(&Some(svec!["HOME"]), false).unwrap() }, + sys: UnaryPermission { + global_state: PermissionState::Prompt, + ..Permissions::new_sys(&Some(svec!["hostname"]), false).unwrap() + }, run: UnaryPermission { global_state: PermissionState::Prompt, ..Permissions::new_run(&Some(svec!["deno"]), false).unwrap() @@ -2754,6 +2963,7 @@ mod tests { assert_eq!(perms.net.query(Some(&("127.0.0.1", None))), PermissionState::Prompt); assert_eq!(perms.net.query(Some(&("127.0.0.1", Some(8000)))), PermissionState::Granted); assert_eq!(perms.env.revoke(Some("HOME")), PermissionState::Prompt); + assert_eq!(perms.env.revoke(Some("hostname")), PermissionState::Prompt); assert_eq!(perms.run.revoke(Some("deno")), PermissionState::Prompt); assert_eq!(perms.ffi.revoke(Some(Path::new("deno"))), PermissionState::Prompt); assert_eq!(perms.hrtime.revoke(), PermissionState::Denied); @@ -2767,6 +2977,7 @@ mod tests { write: Permissions::new_write(&None, true).unwrap(), net: Permissions::new_net(&None, true).unwrap(), env: Permissions::new_env(&None, true).unwrap(), + sys: Permissions::new_sys(&None, true).unwrap(), run: Permissions::new_run(&None, true).unwrap(), ffi: Permissions::new_ffi(&None, true).unwrap(), hrtime: Permissions::new_hrtime(false), @@ -2807,6 +3018,12 @@ mod tests { assert!(perms.env.check("HOME").is_ok()); assert!(perms.env.check("PATH").is_err()); + prompt_value.set(true); + assert!(perms.env.check("hostname").is_ok()); + prompt_value.set(false); + assert!(perms.env.check("hostname").is_ok()); + assert!(perms.env.check("osRelease").is_err()); + assert!(perms.hrtime.check().is_err()); } @@ -2817,6 +3034,7 @@ mod tests { write: Permissions::new_write(&None, true).unwrap(), net: Permissions::new_net(&None, true).unwrap(), env: Permissions::new_env(&None, true).unwrap(), + sys: Permissions::new_sys(&None, true).unwrap(), run: Permissions::new_run(&None, true).unwrap(), ffi: Permissions::new_ffi(&None, true).unwrap(), hrtime: Permissions::new_hrtime(false), @@ -2866,6 +3084,14 @@ mod tests { prompt_value.set(false); assert!(perms.env.check("PATH").is_ok()); + prompt_value.set(false); + assert!(perms.sys.check("hostname", None).is_err()); + prompt_value.set(true); + assert!(perms.sys.check("hostname", None).is_err()); + assert!(perms.sys.check("osRelease", None).is_ok()); + prompt_value.set(false); + assert!(perms.sys.check("osRelease", None).is_ok()); + prompt_value.set(false); assert!(perms.hrtime.check().is_err()); prompt_value.set(true); @@ -2902,6 +3128,7 @@ mod tests { ffi: ChildUnaryPermissionArg::Inherit, read: ChildUnaryPermissionArg::Inherit, run: ChildUnaryPermissionArg::Inherit, + sys: ChildUnaryPermissionArg::Inherit, write: ChildUnaryPermissionArg::Inherit, } ); @@ -2914,6 +3141,7 @@ mod tests { ffi: ChildUnaryPermissionArg::NotGranted, read: ChildUnaryPermissionArg::NotGranted, run: ChildUnaryPermissionArg::NotGranted, + sys: ChildUnaryPermissionArg::NotGranted, write: ChildUnaryPermissionArg::NotGranted, } ); @@ -2966,6 +3194,7 @@ mod tests { "ffi": true, "read": true, "run": true, + "sys": true, "write": true, })) .unwrap(), @@ -2975,6 +3204,7 @@ mod tests { ffi: ChildUnaryPermissionArg::Granted, read: ChildUnaryPermissionArg::Granted, run: ChildUnaryPermissionArg::Granted, + sys: ChildUnaryPermissionArg::Granted, write: ChildUnaryPermissionArg::Granted, ..ChildPermissionsArg::none() } @@ -2986,6 +3216,7 @@ mod tests { "ffi": false, "read": false, "run": false, + "sys": false, "write": false, })) .unwrap(), @@ -2995,6 +3226,7 @@ mod tests { ffi: ChildUnaryPermissionArg::NotGranted, read: ChildUnaryPermissionArg::NotGranted, run: ChildUnaryPermissionArg::NotGranted, + sys: ChildUnaryPermissionArg::NotGranted, write: ChildUnaryPermissionArg::NotGranted, ..ChildPermissionsArg::none() } @@ -3006,6 +3238,7 @@ mod tests { "ffi": ["foo", "file:///bar/baz"], "read": ["foo", "file:///bar/baz"], "run": ["foo", "file:///bar/baz", "./qux"], + "sys": ["hostname", "osRelease"], "write": ["foo", "file:///bar/baz"], })) .unwrap(), @@ -3025,6 +3258,10 @@ mod tests { "file:///bar/baz", "./qux" ]), + sys: ChildUnaryPermissionArg::GrantedList(svec![ + "hostname", + "osRelease" + ]), write: ChildUnaryPermissionArg::GrantedList(svec![ "foo", "file:///bar/baz" @@ -3129,6 +3366,7 @@ mod tests { fn test_handle_empty_value() { assert!(Permissions::new_read(&Some(vec![PathBuf::new()]), false).is_err()); assert!(Permissions::new_env(&Some(vec![String::new()]), false).is_err()); + assert!(Permissions::new_sys(&Some(vec![String::new()]), false).is_err()); assert!(Permissions::new_run(&Some(vec![String::new()]), false).is_err()); assert!(Permissions::new_ffi(&Some(vec![PathBuf::new()]), false).is_err()); assert!(Permissions::new_net(&Some(svec![String::new()]), false).is_err());