From 6fb7e8d93bb9fd8cdd81130a394ae6061930c4f6 Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Thu, 3 Aug 2023 21:19:19 +1000 Subject: [PATCH] feat(permissions): add "--deny-*" flags (#19070) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds new "--deny-*" permission flags. These are complimentary to "--allow-*" flags. These flags can be used to restrict access to certain resources, even if they were granted using "--allow-*" flags or the "--allow-all" ("-A") flag. Eg. specifying "--allow-read --deny-read" will result in a permission error, while "--allow-read --deny-read=/etc" will allow read access to all FS but the "/etc" directory. Runtime permissions APIs ("Deno.permissions") were adjusted as well, mainly by adding, a new "PermissionStatus.partial" field. This field denotes that while permission might be granted to requested resource, it's only partial (ie. a "--deny-*" flag was specified that excludes some of the requested resources). Eg. specifying "--allow-read=foo/ --deny-read=foo/bar" and then querying for permissions like "Deno.permissions.query({ name: "read", path: "foo/" })" will return "PermissionStatus { state: "granted", onchange: null, partial: true }", denoting that some of the subpaths don't have read access. Closes #18804. --------- Co-authored-by: Bartek Iwańczuk Co-authored-by: Nayeem Rahman --- cli/args/flags.rs | 615 ++++- cli/args/{flags_allow_net.rs => flags_net.rs} | 0 cli/args/mod.rs | 10 +- cli/tests/integration/run_tests.rs | 70 +- .../testdata/run/deny_all_permission_args.js | 8 + .../testdata/run/deny_all_permission_args.out | 8 + .../testdata/run/deny_some_permission_args.js | 22 + .../run/deny_some_permission_args.out | 22 + cli/tests/testdata/run/permissions_cache.ts | 5 + cli/tsc/dts/lib.deno.ns.d.ts | 34 +- ext/ffi/call.rs | 4 +- ext/ffi/callback.rs | 2 +- ext/ffi/dlfcn.rs | 2 +- ext/ffi/lib.rs | 2 +- ext/ffi/repr.rs | 40 +- ext/fs/lib.rs | 14 +- ext/fs/ops.rs | 13 +- runtime/build.rs | 10 +- runtime/js/10_permissions.js | 66 +- runtime/ops/permissions.rs | 33 +- runtime/permissions/mod.rs | 2281 +++++++---------- 21 files changed, 1805 insertions(+), 1456 deletions(-) rename cli/args/{flags_allow_net.rs => flags_net.rs} (100%) create mode 100644 cli/tests/testdata/run/deny_all_permission_args.js create mode 100644 cli/tests/testdata/run/deny_all_permission_args.out create mode 100644 cli/tests/testdata/run/deny_some_permission_args.js create mode 100644 cli/tests/testdata/run/deny_some_permission_args.out create mode 100644 cli/tests/testdata/run/permissions_cache.ts diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 9674c68a61..790a9d83fc 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -24,7 +24,7 @@ use std::str::FromStr; use crate::util::fs::canonicalize_path; -use super::flags_allow_net; +use super::flags_net; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct FileFlags { @@ -364,13 +364,21 @@ pub struct Flags { pub allow_all: bool, pub allow_env: Option>, + pub deny_env: Option>, pub allow_hrtime: bool, + pub deny_hrtime: bool, pub allow_net: Option>, + pub deny_net: Option>, pub allow_ffi: Option>, + pub deny_ffi: Option>, pub allow_read: Option>, + pub deny_read: Option>, pub allow_run: Option>, + pub deny_run: Option>, pub allow_sys: Option>, + pub deny_sys: Option>, pub allow_write: Option>, + pub deny_write: Option>, pub ca_stores: Option>, pub ca_data: Option, pub cache_blocklist: Vec, @@ -434,6 +442,17 @@ impl Flags { _ => {} } + match &self.deny_read { + Some(read_denylist) if read_denylist.is_empty() => { + args.push("--deny-read".to_string()); + } + Some(read_denylist) => { + let s = format!("--deny-read={}", join_paths(read_denylist, ",")); + args.push(s); + } + _ => {} + } + match &self.allow_write { Some(write_allowlist) if write_allowlist.is_empty() => { args.push("--allow-write".to_string()); @@ -445,6 +464,17 @@ impl Flags { _ => {} } + match &self.deny_write { + Some(write_denylist) if write_denylist.is_empty() => { + args.push("--deny-write".to_string()); + } + Some(write_denylist) => { + let s = format!("--deny-write={}", join_paths(write_denylist, ",")); + args.push(s); + } + _ => {} + } + match &self.allow_net { Some(net_allowlist) if net_allowlist.is_empty() => { args.push("--allow-net".to_string()); @@ -456,6 +486,17 @@ impl Flags { _ => {} } + match &self.deny_net { + Some(net_denylist) if net_denylist.is_empty() => { + args.push("--deny-net".to_string()); + } + Some(net_denylist) => { + let s = format!("--deny-net={}", net_denylist.join(",")); + args.push(s); + } + _ => {} + } + match &self.unsafely_ignore_certificate_errors { Some(ic_allowlist) if ic_allowlist.is_empty() => { args.push("--unsafely-ignore-certificate-errors".to_string()); @@ -481,6 +522,17 @@ impl Flags { _ => {} } + match &self.deny_env { + Some(env_denylist) if env_denylist.is_empty() => { + args.push("--deny-env".to_string()); + } + Some(env_denylist) => { + let s = format!("--deny-env={}", env_denylist.join(",")); + args.push(s); + } + _ => {} + } + match &self.allow_run { Some(run_allowlist) if run_allowlist.is_empty() => { args.push("--allow-run".to_string()); @@ -492,6 +544,17 @@ impl Flags { _ => {} } + match &self.deny_run { + Some(run_denylist) if run_denylist.is_empty() => { + args.push("--deny-run".to_string()); + } + Some(run_denylist) => { + let s = format!("--deny-run={}", run_denylist.join(",")); + args.push(s); + } + _ => {} + } + match &self.allow_sys { Some(sys_allowlist) if sys_allowlist.is_empty() => { args.push("--allow-sys".to_string()); @@ -503,6 +566,17 @@ impl Flags { _ => {} } + match &self.deny_sys { + Some(sys_denylist) if sys_denylist.is_empty() => { + args.push("--deny-sys".to_string()); + } + Some(sys_denylist) => { + let s = format!("--deny-sys={}", sys_denylist.join(",")); + args.push(s) + } + _ => {} + } + match &self.allow_ffi { Some(ffi_allowlist) if ffi_allowlist.is_empty() => { args.push("--allow-ffi".to_string()); @@ -514,10 +588,25 @@ impl Flags { _ => {} } + match &self.deny_ffi { + Some(ffi_denylist) if ffi_denylist.is_empty() => { + args.push("--deny-ffi".to_string()); + } + Some(ffi_denylist) => { + let s = format!("--deny-ffi={}", join_paths(ffi_denylist, ",")); + args.push(s); + } + _ => {} + } + if self.allow_hrtime { args.push("--allow-hrtime".to_string()); } + if self.deny_hrtime { + args.push("--deny-hrtime".to_string()); + } + args } @@ -607,26 +696,42 @@ impl Flags { pub fn has_permission(&self) -> bool { self.allow_all || self.allow_hrtime + || self.deny_hrtime || self.allow_env.is_some() + || self.deny_env.is_some() || self.allow_ffi.is_some() + || self.deny_ffi.is_some() || self.allow_net.is_some() + || self.deny_net.is_some() || self.allow_read.is_some() + || self.deny_read.is_some() || self.allow_run.is_some() + || self.deny_run.is_some() || self.allow_sys.is_some() + || self.deny_sys.is_some() || self.allow_write.is_some() + || self.deny_write.is_some() } pub fn has_permission_in_argv(&self) -> bool { self.argv.iter().any(|arg| { arg == "--allow-all" || arg == "--allow-hrtime" + || arg == "--deny-hrtime" || arg.starts_with("--allow-env") + || arg.starts_with("--deny-env") || arg.starts_with("--allow-ffi") + || arg.starts_with("--deny-ffi") || arg.starts_with("--allow-net") + || arg.starts_with("--deny-net") || arg.starts_with("--allow-read") + || arg.starts_with("--deny-read") || arg.starts_with("--allow-run") + || arg.starts_with("--deny-run") || arg.starts_with("--allow-sys") + || arg.starts_with("--deny-sys") || arg.starts_with("--allow-write") + || arg.starts_with("--deny-write") }) } } @@ -2037,6 +2142,16 @@ static ALLOW_READ_HELP: &str = concat!( " --allow-read=\"/etc,/var/log.txt\"" ); +static DENY_READ_HELP: &str = concat!( + "Deny file system read access. Optionally specify denied paths.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-read\n", + " --deny-read=\"/etc,/var/log.txt\"" +); + static ALLOW_WRITE_HELP: &str = concat!( "Allow file system write access. Optionally specify allowed paths.\n", "Docs: https://deno.land/manual@v", @@ -2047,6 +2162,16 @@ static ALLOW_WRITE_HELP: &str = concat!( " --allow-write=\"/etc,/var/log.txt\"" ); +static DENY_WRITE_HELP: &str = concat!( + "Deny file system write access. Optionally specify denied paths.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-write\n", + " --deny-write=\"/etc,/var/log.txt\"" +); + static ALLOW_NET_HELP: &str = concat!( "Allow network access. Optionally specify allowed IP addresses and host names, with ports as necessary.\n", "Docs: https://deno.land/manual@v", @@ -2057,6 +2182,16 @@ static ALLOW_NET_HELP: &str = concat!( " --allow-net=\"localhost:8080,deno.land\"" ); +static DENY_NET_HELP: &str = concat!( + "Deny network access. Optionally specify denied IP addresses and host names, with ports as necessary.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-net\n", + " --deny-net=\"localhost:8080,deno.land\"" +); + static ALLOW_ENV_HELP: &str = concat!( "Allow access to system environment information. Optionally specify accessible environment variables.\n", "Docs: https://deno.land/manual@v", @@ -2067,6 +2202,16 @@ static ALLOW_ENV_HELP: &str = concat!( " --allow-env=\"PORT,HOME,PATH\"" ); +static DENY_ENV_HELP: &str = concat!( + "Deny access to system environment information. Optionally specify accessible environment variables.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-env\n", + " --deny-env=\"PORT,HOME,PATH\"" +); + static ALLOW_SYS_HELP: &str = concat!( "Allow access to OS information. Optionally allow specific APIs by function name.\n", "Docs: https://deno.land/manual@v", @@ -2077,6 +2222,16 @@ static ALLOW_SYS_HELP: &str = concat!( " --allow-sys=\"systemMemoryInfo,osRelease\"" ); +static DENY_SYS_HELP: &str = concat!( + "Deny access to OS information. Optionally deny specific APIs by function name.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-sys\n", + " --deny-sys=\"systemMemoryInfo,osRelease\"" +); + static ALLOW_RUN_HELP: &str = concat!( "Allow running subprocesses. Optionally specify allowed runnable program names.\n", "Docs: https://deno.land/manual@v", @@ -2087,6 +2242,16 @@ static ALLOW_RUN_HELP: &str = concat!( " --allow-run=\"whoami,ps\"" ); +static DENY_RUN_HELP: &str = concat!( + "Deny running subprocesses. Optionally specify denied runnable program names.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-run\n", + " --deny-run=\"whoami,ps\"" +); + static ALLOW_FFI_HELP: &str = concat!( "(Unstable) Allow loading dynamic libraries. Optionally specify allowed directories or files.\n", "Docs: https://deno.land/manual@v", @@ -2097,6 +2262,16 @@ static ALLOW_FFI_HELP: &str = concat!( " --allow-ffi=\"./libfoo.so\"" ); +static DENY_FFI_HELP: &str = concat!( + "(Unstable) Deny loading dynamic libraries. Optionally specify denied directories or files.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-ffi\n", + " --deny-ffi=\"./libfoo.so\"" +); + static ALLOW_HRTIME_HELP: &str = concat!( "Allow high-resolution time measurement. Note: this can enable timing attacks and fingerprinting.\n", "Docs: https://deno.land/manual@v", @@ -2104,6 +2279,13 @@ static ALLOW_HRTIME_HELP: &str = concat!( "/basics/permissions\n" ); +static DENY_HRTIME_HELP: &str = concat!( + "Deny high-resolution time measurement. Note: this can prevent timing attacks and fingerprinting.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n" +); + static ALLOW_ALL_HELP: &str = concat!( "Allow all permissions. Learn more about permissions in Deno:\n", "https://deno.land/manual@v", @@ -2124,6 +2306,17 @@ fn permission_args(app: Command) -> Command { .value_parser(value_parser!(PathBuf)) .value_hint(ValueHint::AnyPath), ) + .arg( + Arg::new("deny-read") + .long("deny-read") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("PATH") + .help(DENY_READ_HELP) + .value_parser(value_parser!(PathBuf)) + .value_hint(ValueHint::AnyPath), + ) .arg( Arg::new("allow-write") .long("allow-write") @@ -2135,6 +2328,17 @@ fn permission_args(app: Command) -> Command { .value_parser(value_parser!(PathBuf)) .value_hint(ValueHint::AnyPath), ) + .arg( + Arg::new("deny-write") + .long("deny-write") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("PATH") + .help(DENY_WRITE_HELP) + .value_parser(value_parser!(PathBuf)) + .value_hint(ValueHint::AnyPath), + ) .arg( Arg::new("allow-net") .long("allow-net") @@ -2143,7 +2347,17 @@ fn permission_args(app: Command) -> Command { .require_equals(true) .value_name("IP_OR_HOSTNAME") .help(ALLOW_NET_HELP) - .value_parser(flags_allow_net::validator), + .value_parser(flags_net::validator), + ) + .arg( + Arg::new("deny-net") + .long("deny-net") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("IP_OR_HOSTNAME") + .help(DENY_NET_HELP) + .value_parser(flags_net::validator), ) .arg(unsafely_ignore_certificate_errors_arg()) .arg( @@ -2166,6 +2380,26 @@ fn permission_args(app: Command) -> Command { }) }), ) + .arg( + Arg::new("deny-env") + .long("deny-env") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("VARIABLE_NAME") + .help(DENY_ENV_HELP) + .value_parser(|key: &str| { + if key.is_empty() || key.contains(&['=', '\0'] as &[char]) { + return Err(format!("invalid key \"{key}\"")); + } + + Ok(if cfg!(windows) { + key.to_uppercase() + } else { + key.to_string() + }) + }), + ) .arg( Arg::new("allow-sys") .long("allow-sys") @@ -2176,6 +2410,16 @@ fn permission_args(app: Command) -> Command { .help(ALLOW_SYS_HELP) .value_parser(|key: &str| parse_sys_kind(key).map(ToString::to_string)), ) + .arg( + Arg::new("deny-sys") + .long("deny-sys") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("API_NAME") + .help(DENY_SYS_HELP) + .value_parser(|key: &str| parse_sys_kind(key).map(ToString::to_string)), + ) .arg( Arg::new("allow-run") .long("allow-run") @@ -2185,6 +2429,15 @@ fn permission_args(app: Command) -> Command { .value_name("PROGRAM_NAME") .help(ALLOW_RUN_HELP), ) + .arg( + Arg::new("deny-run") + .long("deny-run") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("PROGRAM_NAME") + .help(DENY_RUN_HELP), + ) .arg( Arg::new("allow-ffi") .long("allow-ffi") @@ -2196,12 +2449,29 @@ fn permission_args(app: Command) -> Command { .value_parser(value_parser!(PathBuf)) .value_hint(ValueHint::AnyPath), ) + .arg( + Arg::new("deny-ffi") + .long("deny-ffi") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("PATH") + .help(DENY_FFI_HELP) + .value_parser(value_parser!(PathBuf)) + .value_hint(ValueHint::AnyPath), + ) .arg( Arg::new("allow-hrtime") .long("allow-hrtime") .action(ArgAction::SetTrue) .help(ALLOW_HRTIME_HELP), ) + .arg( + Arg::new("deny-hrtime") + .long("deny-hrtime") + .action(ArgAction::SetTrue) + .help(DENY_HRTIME_HELP), + ) .arg( Arg::new("allow-all") .short('A') @@ -2594,7 +2864,7 @@ fn unsafely_ignore_certificate_errors_arg() -> Arg { .require_equals(true) .value_name("HOSTNAMES") .help("DANGER: Disables verification of TLS certificates") - .value_parser(flags_allow_net::validator) + .value_parser(flags_net::validator) } fn bench_parse(flags: &mut Flags, matches: &mut ArgMatches) { @@ -3189,38 +3459,76 @@ fn permission_args_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.allow_read = Some(read_wl.collect()); } + if let Some(read_wl) = matches.remove_many::("deny-read") { + flags.deny_read = Some(read_wl.collect()); + } + if let Some(write_wl) = matches.remove_many::("allow-write") { flags.allow_write = Some(write_wl.collect()); } + if let Some(write_wl) = matches.remove_many::("deny-write") { + flags.deny_write = Some(write_wl.collect()); + } + if let Some(net_wl) = matches.remove_many::("allow-net") { - let net_allowlist = flags_allow_net::parse(net_wl.collect()).unwrap(); + let net_allowlist = flags_net::parse(net_wl.collect()).unwrap(); flags.allow_net = Some(net_allowlist); } + if let Some(net_wl) = matches.remove_many::("deny-net") { + let net_denylist = flags_net::parse(net_wl.collect()).unwrap(); + flags.deny_net = Some(net_denylist); + } + if let Some(env_wl) = matches.remove_many::("allow-env") { flags.allow_env = Some(env_wl.collect()); debug!("env allowlist: {:#?}", &flags.allow_env); } + if let Some(env_wl) = matches.remove_many::("deny-env") { + flags.deny_env = Some(env_wl.collect()); + debug!("env denylist: {:#?}", &flags.deny_env); + } + if let Some(run_wl) = matches.remove_many::("allow-run") { flags.allow_run = Some(run_wl.collect()); debug!("run allowlist: {:#?}", &flags.allow_run); } + if let Some(run_wl) = matches.remove_many::("deny-run") { + flags.deny_run = Some(run_wl.collect()); + debug!("run denylist: {:#?}", &flags.deny_run); + } + if let Some(sys_wl) = matches.remove_many::("allow-sys") { flags.allow_sys = Some(sys_wl.collect()); debug!("sys info allowlist: {:#?}", &flags.allow_sys); } + if let Some(sys_wl) = matches.remove_many::("deny-sys") { + flags.deny_sys = Some(sys_wl.collect()); + debug!("sys info denylist: {:#?}", &flags.deny_sys); + } + if let Some(ffi_wl) = matches.remove_many::("allow-ffi") { flags.allow_ffi = Some(ffi_wl.collect()); debug!("ffi allowlist: {:#?}", &flags.allow_ffi); } + if let Some(ffi_wl) = matches.remove_many::("deny-ffi") { + flags.deny_ffi = Some(ffi_wl.collect()); + debug!("ffi denylist: {:#?}", &flags.deny_ffi); + } + if matches.get_flag("allow-hrtime") { flags.allow_hrtime = true; } + + if matches.get_flag("deny-hrtime") { + flags.deny_hrtime = true; + } + if matches.get_flag("allow-all") { flags.allow_all = true; flags.allow_read = Some(vec![]); @@ -3232,6 +3540,7 @@ fn permission_args_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.allow_ffi = Some(vec![]); flags.allow_hrtime = true; } + if matches.get_flag("no-prompt") { flags.no_prompt = true; } @@ -3243,7 +3552,7 @@ fn unsafely_ignore_certificate_errors_parse( if let Some(ic_wl) = matches.remove_many::("unsafely-ignore-certificate-errors") { - let ic_allowlist = flags_allow_net::parse(ic_wl.collect()).unwrap(); + let ic_allowlist = flags_net::parse(ic_wl.collect()).unwrap(); flags.unsafely_ignore_certificate_errors = Some(ic_allowlist); } } @@ -3694,6 +4003,9 @@ mod tests { let r = flags_from_vec(svec!["deno", "run", "--allow-read", "x.ts"]); assert_eq!(r.unwrap().has_permission(), true); + let r = flags_from_vec(svec!["deno", "run", "--deny-read", "x.ts"]); + assert_eq!(r.unwrap().has_permission(), true); + let r = flags_from_vec(svec!["deno", "run", "x.ts"]); assert_eq!(r.unwrap().has_permission(), false); } @@ -3703,6 +4015,9 @@ mod tests { let r = flags_from_vec(svec!["deno", "run", "x.ts", "--allow-read"]); assert_eq!(r.unwrap().has_permission_in_argv(), true); + let r = flags_from_vec(svec!["deno", "run", "x.ts", "--deny-read"]); + assert_eq!(r.unwrap().has_permission_in_argv(), true); + let r = flags_from_vec(svec!["deno", "run", "x.ts"]); assert_eq!(r.unwrap().has_permission_in_argv(), false); } @@ -3771,6 +4086,22 @@ mod tests { ); } + #[test] + fn deny_read() { + let r = flags_from_vec(svec!["deno", "run", "--deny-read", "gist.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "gist.ts".to_string(), + }), + deny_read: Some(vec![]), + ..Flags::default() + } + ); + } + #[test] fn allow_hrtime() { let r = flags_from_vec(svec!["deno", "run", "--allow-hrtime", "gist.ts"]); @@ -3787,6 +4118,22 @@ mod tests { ); } + #[test] + fn deny_hrtime() { + let r = flags_from_vec(svec!["deno", "run", "--deny-hrtime", "gist.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "gist.ts".to_string(), + }), + deny_hrtime: true, + ..Flags::default() + } + ); + } + #[test] fn double_hyphen() { // notice that flags passed after double dash will not @@ -4687,11 +5034,17 @@ mod tests { allow_net: Some(vec![]), unsafely_ignore_certificate_errors: None, allow_env: Some(vec![]), + deny_env: None, allow_run: Some(vec![]), + deny_run: None, allow_read: Some(vec![]), + deny_read: None, allow_sys: Some(vec![]), + deny_sys: None, allow_write: Some(vec![]), + deny_write: None, allow_ffi: Some(vec![]), + deny_ffi: None, allow_hrtime: true, ..Flags::default() } @@ -4804,6 +5157,31 @@ mod tests { ); } + #[test] + fn deny_read_denylist() { + use test_util::TempDir; + let temp_dir_guard = TempDir::new(); + let temp_dir = temp_dir_guard.path().to_path_buf(); + + let r = flags_from_vec(svec![ + "deno", + "run", + format!("--deny-read=.,{}", temp_dir.to_str().unwrap()), + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + deny_read: Some(vec![PathBuf::from("."), temp_dir]), + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + ..Flags::default() + } + ); + } + #[test] fn allow_write_allowlist() { use test_util::TempDir; @@ -4829,6 +5207,31 @@ mod tests { ); } + #[test] + fn deny_write_denylist() { + use test_util::TempDir; + let temp_dir_guard = TempDir::new(); + let temp_dir = temp_dir_guard.path().to_path_buf(); + + let r = flags_from_vec(svec![ + "deno", + "run", + format!("--deny-write=.,{}", temp_dir.to_str().unwrap()), + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + deny_write: Some(vec![PathBuf::from("."), temp_dir]), + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + ..Flags::default() + } + ); + } + #[test] fn allow_net_allowlist() { let r = flags_from_vec(svec![ @@ -4850,6 +5253,23 @@ mod tests { ); } + #[test] + fn deny_net_denylist() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-net=127.0.0.1", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_net: Some(svec!["127.0.0.1"]), + ..Flags::default() + } + ); + } + #[test] fn allow_env_allowlist() { let r = @@ -4867,6 +5287,23 @@ mod tests { ); } + #[test] + fn deny_env_denylist() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-env=HOME", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_env: Some(svec!["HOME"]), + ..Flags::default() + } + ); + } + #[test] fn allow_env_allowlist_multiple() { let r = flags_from_vec(svec![ @@ -4888,6 +5325,23 @@ mod tests { ); } + #[test] + fn deny_env_denylist_multiple() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-env=HOME,PATH", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_env: Some(svec!["HOME", "PATH"]), + ..Flags::default() + } + ); + } + #[test] fn allow_env_allowlist_validator() { let r = @@ -4901,6 +5355,19 @@ mod tests { assert!(r.is_err()); } + #[test] + fn deny_env_denylist_validator() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-env=HOME", "script.ts"]); + assert!(r.is_ok()); + let r = + flags_from_vec(svec!["deno", "run", "--deny-env=H=ME", "script.ts"]); + assert!(r.is_err()); + let r = + flags_from_vec(svec!["deno", "run", "--deny-env=H\0ME", "script.ts"]); + assert!(r.is_err()); + } + #[test] fn allow_sys() { let r = flags_from_vec(svec!["deno", "run", "--allow-sys", "script.ts"]); @@ -4917,6 +5384,22 @@ mod tests { ); } + #[test] + fn deny_sys() { + let r = flags_from_vec(svec!["deno", "run", "--deny-sys", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_sys: Some(vec![]), + ..Flags::default() + } + ); + } + #[test] fn allow_sys_allowlist() { let r = @@ -4934,6 +5417,23 @@ mod tests { ); } + #[test] + fn deny_sys_denylist() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-sys=hostname", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_sys: Some(svec!["hostname"]), + ..Flags::default() + } + ); + } + #[test] fn allow_sys_allowlist_multiple() { let r = flags_from_vec(svec![ @@ -4955,6 +5455,27 @@ mod tests { ); } + #[test] + fn deny_sys_denylist_multiple() { + let r = flags_from_vec(svec![ + "deno", + "run", + "--deny-sys=hostname,osRelease", + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_sys: Some(svec!["hostname", "osRelease"]), + ..Flags::default() + } + ); + } + #[test] fn allow_sys_allowlist_validator() { let r = @@ -4979,6 +5500,29 @@ mod tests { assert!(r.is_err()); } + #[test] + fn deny_sys_denylist_validator() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-sys=hostname", "script.ts"]); + assert!(r.is_ok()); + let r = flags_from_vec(svec![ + "deno", + "run", + "--deny-sys=hostname,osRelease", + "script.ts" + ]); + assert!(r.is_ok()); + let r = flags_from_vec(svec!["deno", "run", "--deny-sys=foo", "script.ts"]); + assert!(r.is_err()); + let r = flags_from_vec(svec![ + "deno", + "run", + "--deny-sys=hostname,foo", + "script.ts" + ]); + assert!(r.is_err()); + } + #[test] fn reload_validator() { let r = flags_from_vec(svec![ @@ -5850,6 +6394,35 @@ mod tests { ); } + #[test] + fn deny_net_denylist_with_ports() { + let r = flags_from_vec(svec![ + "deno", + "run", + "--deny-net=deno.land,:8000,:4545", + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_net: Some(svec![ + "deno.land", + "0.0.0.0:8000", + "127.0.0.1:8000", + "localhost:8000", + "0.0.0.0:4545", + "127.0.0.1:4545", + "localhost:4545" + ]), + ..Flags::default() + } + ); + } + #[test] fn allow_net_allowlist_with_ipv6_address() { let r = flags_from_vec(svec![ @@ -5882,6 +6455,38 @@ mod tests { ); } + #[test] + fn deny_net_denylist_with_ipv6_address() { + let r = flags_from_vec(svec![ + "deno", + "run", + "--deny-net=deno.land,deno.land:80,::,127.0.0.1,[::1],1.2.3.4:5678,:5678,[::1]:8080", + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_net: Some(svec![ + "deno.land", + "deno.land:80", + "::", + "127.0.0.1", + "[::1]", + "1.2.3.4:5678", + "0.0.0.0:5678", + "127.0.0.1:5678", + "localhost:5678", + "[::1]:8080" + ]), + ..Flags::default() + } + ); + } + #[test] fn lock_write() { let r = flags_from_vec(svec![ diff --git a/cli/args/flags_allow_net.rs b/cli/args/flags_net.rs similarity index 100% rename from cli/args/flags_allow_net.rs rename to cli/args/flags_net.rs diff --git a/cli/args/mod.rs b/cli/args/mod.rs index cea0c0ca14..7b3b0aa832 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -2,7 +2,7 @@ mod config_file; mod flags; -mod flags_allow_net; +mod flags_net; mod import_map; mod lockfile; pub mod package_json; @@ -1105,13 +1105,21 @@ impl CliOptions { pub fn permissions_options(&self) -> PermissionsOptions { PermissionsOptions { allow_env: self.flags.allow_env.clone(), + deny_env: self.flags.deny_env.clone(), allow_hrtime: self.flags.allow_hrtime, + deny_hrtime: self.flags.deny_hrtime, allow_net: self.flags.allow_net.clone(), + deny_net: self.flags.deny_net.clone(), allow_ffi: self.flags.allow_ffi.clone(), + deny_ffi: self.flags.deny_ffi.clone(), allow_read: self.flags.allow_read.clone(), + deny_read: self.flags.deny_read.clone(), allow_run: self.flags.allow_run.clone(), + deny_run: self.flags.deny_run.clone(), allow_sys: self.flags.allow_sys.clone(), + deny_sys: self.flags.deny_sys.clone(), allow_write: self.flags.allow_write.clone(), + deny_write: self.flags.deny_write.clone(), prompt: !self.no_prompt(), } } diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index 9720f25776..e839320110 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -601,7 +601,7 @@ fn _090_run_permissions_request() { .with_pty(|mut console| { console.expect(concat!( "┌ ⚠️ Deno requests run access to \"ls\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -609,7 +609,7 @@ fn _090_run_permissions_request() { console.expect("Granted run access to \"ls\"."); console.expect(concat!( "┌ ⚠️ Deno requests run access to \"cat\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -628,7 +628,7 @@ fn _090_run_permissions_request_sync() { .with_pty(|mut console| { console.expect(concat!( "┌ ⚠️ Deno requests run access to \"ls\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -636,7 +636,7 @@ fn _090_run_permissions_request_sync() { console.expect("Granted run access to \"ls\"."); console.expect(concat!( "┌ ⚠️ Deno requests run access to \"cat\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -656,7 +656,7 @@ fn permissions_prompt_allow_all() { // "run" permissions console.expect(concat!( "┌ ⚠️ Deno requests run access to \"FOO\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -665,7 +665,7 @@ fn permissions_prompt_allow_all() { // "read" permissions console.expect(concat!( "┌ ⚠️ Deno requests read access to \"FOO\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -674,7 +674,7 @@ fn permissions_prompt_allow_all() { // "write" permissions console.expect(concat!( "┌ ⚠️ Deno requests write access to \"FOO\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -682,8 +682,8 @@ fn permissions_prompt_allow_all() { console.expect("✅ Granted all write access."); // "net" permissions console.expect(concat!( - "┌ ⚠️ Deno requests network access to \"foo\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "┌ ⚠️ Deno requests net access to \"foo\".\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -692,7 +692,7 @@ fn permissions_prompt_allow_all() { // "env" permissions console.expect(concat!( "┌ ⚠️ Deno requests env access to \"FOO\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -701,7 +701,7 @@ fn permissions_prompt_allow_all() { // "sys" permissions console.expect(concat!( "┌ ⚠️ Deno requests sys access to \"loadavg\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -710,7 +710,7 @@ fn permissions_prompt_allow_all() { // "ffi" permissions console.expect(concat!( "┌ ⚠️ Deno requests ffi access to \"FOO\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -766,7 +766,7 @@ fn permissions_prompt_allow_all_lowercase_a() { // "run" permissions console.expect(concat!( "┌ ⚠️ Deno requests run access to \"FOO\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -775,6 +775,36 @@ fn permissions_prompt_allow_all_lowercase_a() { }); } +itest!(deny_all_permission_args { + args: "run --deny-env --deny-read --deny-write --deny-ffi --deny-run --deny-sys --deny-net --deny-hrtime run/deny_all_permission_args.js", + output: "run/deny_all_permission_args.out", +}); + +itest!(deny_some_permission_args { + args: "run --allow-env --deny-env=FOO --allow-read --deny-read=/foo --allow-write --deny-write=/foo --allow-ffi --deny-ffi=/foo --allow-run --deny-run=foo --allow-sys --deny-sys=hostname --allow-net --deny-net=127.0.0.1 --allow-hrtime --deny-hrtime run/deny_some_permission_args.js", + output: "run/deny_some_permission_args.out", +}); + +#[test] +fn permissions_cache() { + TestContext::default() + .new_command() + .args_vec(["run", "--quiet", "run/permissions_cache.ts"]) + .with_pty(|mut console| { + console.expect(concat!( + "prompt\r\n", + "┌ ⚠️ Deno requests read access to \"foo\".\r\n", + "├ Requested by `Deno.permissions.request()` 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)", + )); + console.write_line_raw("y"); + console.expect("✅ Granted read access to \"foo\"."); + console.expect("granted"); + console.expect("prompt"); + }); +} + 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", @@ -2541,14 +2571,14 @@ mod permissions { .with_pty(|mut console| { console.expect(concat!( "┌ ⚠️ Deno requests read access to \"foo\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); console.write_line_raw("y"); console.expect(concat!( "┌ ⚠️ Deno requests read access to \"bar\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -2567,14 +2597,14 @@ mod permissions { .with_pty(|mut console| { console.expect(concat!( "┌ ⚠️ Deno requests read access to \"foo\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); console.write_line_raw("y"); console.expect(concat!( "┌ ⚠️ Deno requests read access to \"bar\".\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -2593,7 +2623,7 @@ mod permissions { .with_pty(|mut console| { console.expect(concat!( "┌ ⚠️ Deno requests read access.\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -2615,7 +2645,7 @@ mod permissions { .with_pty(|mut console| { console.expect(concat!( "┌ ⚠️ Deno requests read access.\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); @@ -2756,7 +2786,7 @@ fn issue9750() { console.write_line_raw("yy"); console.expect(concat!( "┌ ⚠️ Deno requests env access.\r\n", - "├ Requested by `Deno.permissions.query()` API.\r\n", + "├ Requested by `Deno.permissions.request()` 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)", )); diff --git a/cli/tests/testdata/run/deny_all_permission_args.js b/cli/tests/testdata/run/deny_all_permission_args.js new file mode 100644 index 0000000000..b0ca864fbd --- /dev/null +++ b/cli/tests/testdata/run/deny_all_permission_args.js @@ -0,0 +1,8 @@ +console.log(Deno.permissions.querySync({ name: "env" })); +console.log(Deno.permissions.querySync({ name: "read" })); +console.log(Deno.permissions.querySync({ name: "write" })); +console.log(Deno.permissions.querySync({ name: "ffi" })); +console.log(Deno.permissions.querySync({ name: "run" })); +console.log(Deno.permissions.querySync({ name: "sys" })); +console.log(Deno.permissions.querySync({ name: "net" })); +console.log(Deno.permissions.querySync({ name: "hrtime" })); diff --git a/cli/tests/testdata/run/deny_all_permission_args.out b/cli/tests/testdata/run/deny_all_permission_args.out new file mode 100644 index 0000000000..2a5228d620 --- /dev/null +++ b/cli/tests/testdata/run/deny_all_permission_args.out @@ -0,0 +1,8 @@ +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "denied", onchange: null } diff --git a/cli/tests/testdata/run/deny_some_permission_args.js b/cli/tests/testdata/run/deny_some_permission_args.js new file mode 100644 index 0000000000..320376b6fe --- /dev/null +++ b/cli/tests/testdata/run/deny_some_permission_args.js @@ -0,0 +1,22 @@ +console.log(Deno.permissions.querySync({ name: "env" })); +console.log(Deno.permissions.querySync({ name: "env", variable: "FOO" })); +console.log(Deno.permissions.querySync({ name: "env", variable: "BAR" })); +console.log(Deno.permissions.querySync({ name: "read" })); +console.log(Deno.permissions.querySync({ name: "read", path: "/foo" })); +console.log(Deno.permissions.querySync({ name: "read", path: "/bar" })); +console.log(Deno.permissions.querySync({ name: "write" })); +console.log(Deno.permissions.querySync({ name: "write", path: "/foo" })); +console.log(Deno.permissions.querySync({ name: "write", path: "/bar" })); +console.log(Deno.permissions.querySync({ name: "ffi" })); +console.log(Deno.permissions.querySync({ name: "ffi", path: "/foo" })); +console.log(Deno.permissions.querySync({ name: "ffi", path: "/bar" })); +console.log(Deno.permissions.querySync({ name: "run" })); +console.log(Deno.permissions.querySync({ name: "run", command: "foo" })); +console.log(Deno.permissions.querySync({ name: "run", command: "bar" })); +console.log(Deno.permissions.querySync({ name: "sys" })); +console.log(Deno.permissions.querySync({ name: "sys", kind: "hostname" })); +console.log(Deno.permissions.querySync({ name: "sys", kind: "loadavg" })); +console.log(Deno.permissions.querySync({ name: "net" })); +console.log(Deno.permissions.querySync({ name: "net", host: "127.0.0.1" })); +console.log(Deno.permissions.querySync({ name: "net", host: "192.168.0.1" })); +console.log(Deno.permissions.querySync({ name: "hrtime" })); diff --git a/cli/tests/testdata/run/deny_some_permission_args.out b/cli/tests/testdata/run/deny_some_permission_args.out new file mode 100644 index 0000000000..80c37159ba --- /dev/null +++ b/cli/tests/testdata/run/deny_some_permission_args.out @@ -0,0 +1,22 @@ +PermissionStatus { state: "granted", onchange: null, partial: true } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "granted", onchange: null } +PermissionStatus { state: "granted", onchange: null, partial: true } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "granted", onchange: null } +PermissionStatus { state: "granted", onchange: null, partial: true } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "granted", onchange: null } +PermissionStatus { state: "granted", onchange: null, partial: true } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "granted", onchange: null } +PermissionStatus { state: "granted", onchange: null, partial: true } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "granted", onchange: null } +PermissionStatus { state: "granted", onchange: null, partial: true } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "granted", onchange: null } +PermissionStatus { state: "granted", onchange: null, partial: true } +PermissionStatus { state: "denied", onchange: null } +PermissionStatus { state: "granted", onchange: null } +PermissionStatus { state: "denied", onchange: null } diff --git a/cli/tests/testdata/run/permissions_cache.ts b/cli/tests/testdata/run/permissions_cache.ts new file mode 100644 index 0000000000..c77ee0f364 --- /dev/null +++ b/cli/tests/testdata/run/permissions_cache.ts @@ -0,0 +1,5 @@ +const status = await Deno.permissions.query({ name: "read", path: "foo" }); +console.log(status.state); +status.onchange = () => console.log(status.state); +await Deno.permissions.request({ name: "read", path: "foo" }); // y +await Deno.permissions.revoke({ name: "read", path: "foo" }); diff --git a/cli/tsc/dts/lib.deno.ns.d.ts b/cli/tsc/dts/lib.deno.ns.d.ts index 1c8d9db63c..436387ebaa 100644 --- a/cli/tsc/dts/lib.deno.ns.d.ts +++ b/cli/tsc/dts/lib.deno.ns.d.ts @@ -4403,9 +4403,12 @@ declare namespace Deno { * * @category Permissions */ - export type PermissionState = "granted" | "denied" | "prompt"; + export type PermissionState = + | "granted" + | "denied" + | "prompt"; - /** The permission descriptor for the `allow-run` permission, which controls + /** The permission descriptor for the `allow-run` and `deny-run` permissions, which controls * access to what sub-processes can be executed by Deno. The option `command` * allows scoping the permission to a specific executable. * @@ -4416,12 +4419,12 @@ declare namespace Deno { * @category Permissions */ export interface RunPermissionDescriptor { name: "run"; - /** The `allow-run` permission can be scoped to a specific executable, + /** An `allow-run` or `deny-run` permission can be scoped to a specific executable, * which would be relative to the start-up CWD of the Deno CLI. */ command?: string | URL; } - /** The permission descriptor for the `allow-read` permissions, which controls + /** The permission descriptor for the `allow-read` and `deny-read` permissions, which controls * access to reading resources from the local host. The option `path` allows * scoping the permission to a specific path (and if the path is a directory * any sub paths). @@ -4432,12 +4435,12 @@ declare namespace Deno { * @category Permissions */ export interface ReadPermissionDescriptor { name: "read"; - /** The `allow-read` permission can be scoped to a specific path (and if + /** An `allow-read` or `deny-read` permission can be scoped to a specific path (and if * the path is a directory, any sub paths). */ path?: string | URL; } - /** The permission descriptor for the `allow-write` permissions, which + /** The permission descriptor for the `allow-write` and `deny-write` permissions, which * controls access to writing to resources from the local host. The option * `path` allow scoping the permission to a specific path (and if the path is * a directory any sub paths). @@ -4448,12 +4451,12 @@ declare namespace Deno { * @category Permissions */ export interface WritePermissionDescriptor { name: "write"; - /** The `allow-write` permission can be scoped to a specific path (and if + /** An `allow-write` or `deny-write` permission can be scoped to a specific path (and if * the path is a directory, any sub paths). */ path?: string | URL; } - /** The permission descriptor for the `allow-net` permissions, which controls + /** The permission descriptor for the `allow-net` and `deny-net` permissions, which controls * access to opening network ports and connecting to remote hosts via the * network. The option `host` allows scoping the permission for outbound * connection to a specific host and port. @@ -4469,7 +4472,7 @@ declare namespace Deno { host?: string; } - /** The permission descriptor for the `allow-env` permissions, which controls + /** The permission descriptor for the `allow-env` and `deny-env` permissions, which controls * access to being able to read and write to the process environment variables * as well as access other information about the environment. The option * `variable` allows scoping the permission to a specific environment @@ -4482,7 +4485,7 @@ declare namespace Deno { variable?: string; } - /** The permission descriptor for the `allow-sys` permissions, which controls + /** The permission descriptor for the `allow-sys` and `deny-sys` permissions, which controls * access to sensitive host system information, which malicious code might * attempt to exploit. The option `kind` allows scoping the permission to a * specific piece of information. @@ -4502,7 +4505,7 @@ declare namespace Deno { | "gid"; } - /** The permission descriptor for the `allow-ffi` permissions, which controls + /** The permission descriptor for the `allow-ffi` and `deny-ffi` permissions, which controls * access to loading _foreign_ code and interfacing with it via the * [Foreign Function Interface API](https://deno.land/manual/runtime/ffi_api) * available in Deno. The option `path` allows scoping the permission to a @@ -4515,7 +4518,7 @@ declare namespace Deno { path?: string | URL; } - /** The permission descriptor for the `allow-hrtime` permission, which + /** The permission descriptor for the `allow-hrtime` and `deny-hrtime` permissions, which * controls if the runtime code has access to high resolution time. High * resolution time is considered sensitive information, because it can be used * by malicious code to gain information about the host that it might not @@ -4560,6 +4563,13 @@ declare namespace Deno { // deno-lint-ignore no-explicit-any onchange: ((this: PermissionStatus, ev: Event) => any) | null; readonly state: PermissionState; + /** + * Describes if permission is only granted partially, eg. an access + * might be granted to "/foo" directory, but denied for "/foo/bar". + * In such case this field will be set to `true` when querying for + * read permissions of "/foo" directory. + */ + readonly partial: boolean; addEventListener( type: K, listener: ( diff --git a/ext/ffi/call.rs b/ext/ffi/call.rs index 21358d851e..82f8dd8223 100644 --- a/ext/ffi/call.rs +++ b/ext/ffi/call.rs @@ -289,7 +289,7 @@ where { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; }; let symbol = PtrSymbol::new(pointer, &def)?; @@ -389,7 +389,7 @@ where { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; }; let symbol = PtrSymbol::new(pointer, &def)?; diff --git a/ext/ffi/callback.rs b/ext/ffi/callback.rs index e9d91855da..9a36815d0c 100644 --- a/ext/ffi/callback.rs +++ b/ext/ffi/callback.rs @@ -546,7 +546,7 @@ where { check_unstable(state, "Deno.UnsafeCallback"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; let v8_value = cb.v8_value; let cb = v8::Local::::try_from(v8_value)?; diff --git a/ext/ffi/dlfcn.rs b/ext/ffi/dlfcn.rs index 3af9177bf8..ced64bec53 100644 --- a/ext/ffi/dlfcn.rs +++ b/ext/ffi/dlfcn.rs @@ -144,7 +144,7 @@ where check_unstable(state, "Deno.dlopen"); let permissions = state.borrow_mut::(); - permissions.check(Some(&PathBuf::from(&path)))?; + permissions.check_partial(Some(&PathBuf::from(&path)))?; let lib = Library::open(&path).map_err(|e| { dlopen::Error::OpeningLibraryError(std::io::Error::new( diff --git a/ext/ffi/lib.rs b/ext/ffi/lib.rs index 2d3bb2866f..b5a2a3bd57 100644 --- a/ext/ffi/lib.rs +++ b/ext/ffi/lib.rs @@ -64,7 +64,7 @@ pub fn check_unstable2(state: &Rc>, api_name: &str) { } pub trait FfiPermissions { - fn check(&mut self, path: Option<&Path>) -> Result<(), AnyError>; + fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError>; } pub(crate) type PendingFfiAsyncWork = Box; diff --git a/ext/ffi/repr.rs b/ext/ffi/repr.rs index 0e2f88084d..665e37186f 100644 --- a/ext/ffi/repr.rs +++ b/ext/ffi/repr.rs @@ -24,7 +24,7 @@ where { check_unstable(state, "Deno.UnsafePointer#create"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; Ok(ptr_number as *mut c_void) } @@ -40,7 +40,7 @@ where { check_unstable(state, "Deno.UnsafePointer#equals"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; Ok(a == b) } @@ -55,7 +55,7 @@ where { check_unstable(state, "Deno.UnsafePointer#of"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; Ok(buf as *mut c_void) } @@ -71,7 +71,7 @@ where { check_unstable(state, "Deno.UnsafePointer#offset"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid pointer to offset, pointer is null")); @@ -99,7 +99,7 @@ where { check_unstable(state, "Deno.UnsafePointer#value"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; let outptr = out.as_ptr() as *mut usize; let length = out.len(); @@ -129,7 +129,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getArrayBuffer"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid ArrayBuffer pointer, pointer is null")); @@ -164,7 +164,7 @@ where check_unstable(state, "Deno.UnsafePointerView#copyInto"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if src.is_null() { Err(type_error("Invalid ArrayBuffer pointer, pointer is null")) @@ -197,7 +197,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getCString"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid CString pointer, pointer is null")); @@ -227,7 +227,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getBool"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid bool pointer, pointer is null")); @@ -249,7 +249,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getUint8"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid u8 pointer, pointer is null")); @@ -273,7 +273,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getInt8"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid i8 pointer, pointer is null")); @@ -297,7 +297,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getUint16"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid u16 pointer, pointer is null")); @@ -321,7 +321,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getInt16"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid i16 pointer, pointer is null")); @@ -345,7 +345,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getUint32"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid u32 pointer, pointer is null")); @@ -367,7 +367,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getInt32"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid i32 pointer, pointer is null")); @@ -390,7 +390,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getBigUint64"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; let outptr = out.as_mut_ptr() as *mut u64; @@ -425,7 +425,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getBigUint64"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; let outptr = out.as_mut_ptr() as *mut i64; @@ -458,7 +458,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getFloat32"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid f32 pointer, pointer is null")); @@ -480,7 +480,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getFloat64"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid f64 pointer, pointer is null")); @@ -502,7 +502,7 @@ where check_unstable(state, "Deno.UnsafePointerView#getPointer"); let permissions = state.borrow_mut::(); - permissions.check(None)?; + permissions.check_partial(None)?; if ptr.is_null() { return Err(type_error("Invalid pointer pointer, pointer is null")); diff --git a/ext/fs/lib.rs b/ext/fs/lib.rs index d277129271..b028b12c16 100644 --- a/ext/fs/lib.rs +++ b/ext/fs/lib.rs @@ -23,7 +23,8 @@ use std::path::Path; use std::rc::Rc; pub trait FsPermissions { - fn check_read(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError>; + fn check_read(&mut self, path: &Path, api_name: &str) + -> Result<(), AnyError>; fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError>; fn check_read_blind( &mut self, @@ -31,7 +32,16 @@ pub trait FsPermissions { display: &str, api_name: &str, ) -> Result<(), AnyError>; - fn check_write(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError>; + fn check_write( + &mut self, + path: &Path, + api_name: &str, + ) -> Result<(), AnyError>; + fn check_write_partial( + &mut self, + path: &Path, + api_name: &str, + ) -> Result<(), AnyError>; fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError>; fn check_write_blind( &mut self, diff --git a/ext/fs/ops.rs b/ext/fs/ops.rs index 083d1b15f9..da52318a45 100644 --- a/ext/fs/ops.rs +++ b/ext/fs/ops.rs @@ -294,9 +294,16 @@ where let fs = { let mut state = state.borrow_mut(); - state - .borrow_mut::

() - .check_write(&path, "Deno.remove()")?; + if recursive { + state + .borrow_mut::

() + .check_write(&path, "Deno.remove()")?; + } else { + state + .borrow_mut::

() + .check_write_partial(&path, "Deno.remove()")?; + } + state.borrow::().clone() }; diff --git a/runtime/build.rs b/runtime/build.rs index 828bc3c536..5b02f22022 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -111,7 +111,7 @@ mod startup_snapshot { } impl deno_ffi::FfiPermissions for Permissions { - fn check( + fn check_partial( &mut self, _path: Option<&Path>, ) -> Result<(), deno_core::error::AnyError> { @@ -204,6 +204,14 @@ mod startup_snapshot { unreachable!("snapshotting!") } + fn check_write_partial( + &mut self, + _path: &Path, + _api_name: &str, + ) -> Result<(), AnyError> { + unreachable!("snapshotting!") + } + fn check_write_all(&mut self, _api_name: &str) -> Result<(), AnyError> { unreachable!("snapshotting!") } diff --git a/runtime/js/10_permissions.js b/runtime/js/10_permissions.js index 79cbb76321..3004ab694c 100644 --- a/runtime/js/10_permissions.js +++ b/runtime/js/10_permissions.js @@ -30,6 +30,7 @@ const illegalConstructorKey = Symbol("illegalConstructorKey"); * @typedef StatusCacheValue * @property {PermissionState} state * @property {PermissionStatus} status + * @property {boolean} partial */ /** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "sys" | "run" | "ffi" | "hrtime">} */ @@ -69,27 +70,32 @@ function opRequest(desc) { } class PermissionStatus extends EventTarget { - /** @type {{ state: Deno.PermissionState }} */ - #state; + /** @type {{ state: Deno.PermissionState, partial: boolean }} */ + #status; /** @type {((this: PermissionStatus, event: Event) => any) | null} */ onchange = null; /** @returns {Deno.PermissionState} */ get state() { - return this.#state.state; + return this.#status.state; + } + + /** @returns {boolean} */ + get partial() { + return this.#status.partial; } /** - * @param {{ state: Deno.PermissionState }} state + * @param {{ state: Deno.PermissionState, partial: boolean }} status * @param {unknown} key */ - constructor(state = null, key = null) { + constructor(status = null, key = null) { if (key != illegalConstructorKey) { throw new TypeError("Illegal constructor."); } super(); - this.#state = state; + this.#status = status; } /** @@ -106,9 +112,9 @@ class PermissionStatus extends EventTarget { } [SymbolFor("Deno.privateCustomInspect")](inspect) { - return `${this.constructor.name} ${ - inspect({ state: this.state, onchange: this.onchange }) - }`; + const object = { state: this.state, onchange: this.onchange }; + if (this.partial) object.partial = this.partial; + return `${this.constructor.name} ${inspect(object)}`; } } @@ -117,10 +123,10 @@ const statusCache = new SafeMap(); /** * @param {Deno.PermissionDescriptor} desc - * @param {Deno.PermissionState} state + * @param {{ state: Deno.PermissionState, partial: boolean }} rawStatus * @returns {PermissionStatus} */ -function cache(desc, state) { +function cache(desc, rawStatus) { let { name: key } = desc; if ( (desc.name === "read" || desc.name === "write" || desc.name === "ffi") && @@ -139,18 +145,24 @@ function cache(desc, state) { key += "$"; } if (MapPrototypeHas(statusCache, key)) { - const status = MapPrototypeGet(statusCache, key); - if (status.state !== state) { - status.state = state; - status.status.dispatchEvent(new Event("change", { cancelable: false })); + const cachedObj = MapPrototypeGet(statusCache, key); + if ( + cachedObj.state !== rawStatus.state || + cachedObj.partial !== rawStatus.partial + ) { + cachedObj.state = rawStatus.state; + cachedObj.partial = rawStatus.partial; + cachedObj.status.dispatchEvent( + new Event("change", { cancelable: false }), + ); } - return status.status; + return cachedObj.status; } - /** @type {{ state: Deno.PermissionState; status?: PermissionStatus }} */ - const status = { state }; - status.status = new PermissionStatus(status, illegalConstructorKey); - MapPrototypeSet(statusCache, key, status); - return status.status; + /** @type {{ state: Deno.PermissionState, partial: boolean, status?: PermissionStatus }} */ + const obj = rawStatus; + obj.status = new PermissionStatus(obj, illegalConstructorKey); + MapPrototypeSet(statusCache, key, obj); + return obj.status; } /** @@ -200,8 +212,8 @@ class Permissions { formDescriptor(desc); - const state = opQuery(desc); - return cache(desc, state); + const status = opQuery(desc); + return cache(desc, status); } revoke(desc) { @@ -221,8 +233,8 @@ class Permissions { formDescriptor(desc); - const state = opRevoke(desc); - return cache(desc, state); + const status = opRevoke(desc); + return cache(desc, status); } request(desc) { @@ -242,8 +254,8 @@ class Permissions { formDescriptor(desc); - const state = opRequest(desc); - return cache(desc, state); + const status = opRequest(desc); + return cache(desc, status); } } diff --git a/runtime/ops/permissions.rs b/runtime/ops/permissions.rs index 663b1d2409..e5c55076c5 100644 --- a/runtime/ops/permissions.rs +++ b/runtime/ops/permissions.rs @@ -1,6 +1,7 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use crate::permissions::parse_sys_kind; +use crate::permissions::PermissionState; use crate::permissions::PermissionsContainer; use deno_core::error::custom_error; use deno_core::error::uri_error; @@ -9,6 +10,7 @@ use deno_core::op; use deno_core::url; use deno_core::OpState; use serde::Deserialize; +use serde::Serialize; use std::path::Path; deno_core::extension!( @@ -30,11 +32,30 @@ pub struct PermissionArgs { command: Option, } +#[derive(Serialize)] +pub struct PermissionStatus { + state: String, + partial: bool, +} + +impl From for PermissionStatus { + fn from(state: PermissionState) -> Self { + PermissionStatus { + state: if state == PermissionState::GrantedPartial { + PermissionState::Granted.to_string() + } else { + state.to_string() + }, + partial: state == PermissionState::GrantedPartial, + } + } +} + #[op] pub fn op_query_permission( state: &mut OpState, args: PermissionArgs, -) -> Result { +) -> Result { let permissions = state.borrow::().0.lock(); let path = args.path.as_deref(); let perm = match args.name.as_ref() { @@ -61,14 +82,14 @@ pub fn op_query_permission( )) } }; - Ok(perm.to_string()) + Ok(PermissionStatus::from(perm)) } #[op] pub fn op_revoke_permission( state: &mut OpState, args: PermissionArgs, -) -> Result { +) -> Result { let mut permissions = state.borrow_mut::().0.lock(); let path = args.path.as_deref(); let perm = match args.name.as_ref() { @@ -95,14 +116,14 @@ pub fn op_revoke_permission( )) } }; - Ok(perm.to_string()) + Ok(PermissionStatus::from(perm)) } #[op] pub fn op_request_permission( state: &mut OpState, args: PermissionArgs, -) -> Result { +) -> Result { let mut permissions = state.borrow_mut::().0.lock(); let path = args.path.as_deref(); let perm = match args.name.as_ref() { @@ -129,7 +150,7 @@ pub fn op_request_permission( )) } }; - Ok(perm.to_string()) + Ok(PermissionStatus::from(perm)) } fn parse_host(host_str: &str) -> Result<(String, Option), AnyError> { diff --git a/runtime/permissions/mod.rs b/runtime/permissions/mod.rs index 56ebf83273..93294fc926 100644 --- a/runtime/permissions/mod.rs +++ b/runtime/permissions/mod.rs @@ -39,15 +39,41 @@ pub use prompter::PromptCallback; static DEBUG_LOG_ENABLED: Lazy = Lazy::new(|| log::log_enabled!(log::Level::Debug)); -/// Tri-state value for storing permission state +/// Quadri-state value for storing permission state #[derive( Eq, PartialEq, Default, Debug, Clone, Copy, Deserialize, PartialOrd, )] pub enum PermissionState { Granted = 0, + GrantedPartial = 1, #[default] - Prompt = 1, - Denied = 2, + Prompt = 2, + Denied = 3, +} + +/// `AllowPartial` prescribes how to treat a permission which is partially +/// denied due to a `--deny-*` flag affecting a subscope of the queried +/// permission. +/// +/// `TreatAsGranted` is used in place of `TreatAsPartialGranted` when we don't +/// want to wastefully check for partial denials when, say, checking read +/// access for a file. +#[derive(Debug, Eq, PartialEq)] +#[allow(clippy::enum_variant_names)] +enum AllowPartial { + TreatAsGranted, + TreatAsDenied, + TreatAsPartialGranted, +} + +impl From for AllowPartial { + fn from(value: bool) -> Self { + if value { + Self::TreatAsGranted + } else { + Self::TreatAsDenied + } + } } impl PermissionState { @@ -143,6 +169,7 @@ impl fmt::Display for PermissionState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PermissionState::Granted => f.pad("granted"), + PermissionState::GrantedPartial => f.pad("granted-partial"), PermissionState::Prompt => f.pad("prompt"), PermissionState::Denied => f.pad("denied"), } @@ -226,22 +253,263 @@ impl AsRef for EnvVarName { } } +pub trait Descriptor: Eq + Clone { + fn flag_name() -> &'static str; + fn name(&self) -> Cow; + // By default, specifies no-stronger-than relationship. + // As this is not strict, it's only true when descriptors are the same. + fn stronger_than(&self, other: &Self) -> bool { + self == other + } +} + #[derive(Clone, Debug, Eq, PartialEq)] -pub struct UnaryPermission { - pub name: &'static str, - pub description: &'static str, - pub global_state: PermissionState, +pub struct UnaryPermission { + pub granted_global: bool, pub granted_list: HashSet, - pub denied_list: HashSet, + pub flag_denied_global: bool, + pub flag_denied_list: HashSet, + pub prompt_denied_global: bool, + pub prompt_denied_list: HashSet, pub prompt: bool, } +impl Default for UnaryPermission { + fn default() -> Self { + UnaryPermission { + granted_global: Default::default(), + granted_list: Default::default(), + flag_denied_global: Default::default(), + flag_denied_list: Default::default(), + prompt_denied_global: Default::default(), + prompt_denied_list: Default::default(), + prompt: Default::default(), + } + } +} + +impl UnaryPermission { + fn check_desc( + &mut self, + desc: &Option, + assert_non_partial: bool, + api_name: Option<&str>, + get_display_name: impl Fn() -> Option, + ) -> Result<(), AnyError> { + let (result, prompted, is_allow_all) = self + .query_desc(desc, AllowPartial::from(assert_non_partial)) + .check2( + T::flag_name(), + api_name, + || match get_display_name() { + Some(display_name) => Some(display_name), + None => desc.as_ref().map(|d| format!("\"{}\"", d.name())), + }, + self.prompt, + ); + if prompted { + if result.is_ok() { + if is_allow_all { + self.insert_granted(None); + } else { + self.insert_granted(desc.clone()); + } + } else { + self.insert_prompt_denied(desc.clone()); + } + } + result + } + + fn query_desc( + &self, + desc: &Option, + allow_partial: AllowPartial, + ) -> PermissionState { + if self.is_flag_denied(desc) || self.is_prompt_denied(desc) { + PermissionState::Denied + } else if self.is_granted(desc) { + match allow_partial { + AllowPartial::TreatAsGranted => PermissionState::Granted, + AllowPartial::TreatAsDenied => { + if self.is_partial_flag_denied(desc) { + PermissionState::Denied + } else { + PermissionState::Granted + } + } + AllowPartial::TreatAsPartialGranted => { + if self.is_partial_flag_denied(desc) { + PermissionState::GrantedPartial + } else { + PermissionState::Granted + } + } + } + } else if matches!(allow_partial, AllowPartial::TreatAsDenied) + && self.is_partial_flag_denied(desc) + { + PermissionState::Denied + } else { + PermissionState::Prompt + } + } + + fn request_desc( + &mut self, + desc: &Option, + get_display_name: impl Fn() -> Option, + ) -> PermissionState { + let state = self.query_desc(desc, AllowPartial::TreatAsPartialGranted); + if state == PermissionState::Granted { + self.insert_granted(desc.clone()); + return state; + } + if state != PermissionState::Prompt { + return state; + } + let mut message = String::with_capacity(40); + message.push_str(&format!("{} access", T::flag_name())); + match get_display_name() { + Some(display_name) => { + message.push_str(&format!(" to \"{}\"", display_name)) + } + None => match desc { + Some(desc) => message.push_str(&format!(" to \"{}\"", desc.name())), + None => {} + }, + } + match permission_prompt( + &message, + T::flag_name(), + Some("Deno.permissions.request()"), + true, + ) { + PromptResponse::Allow => { + self.insert_granted(desc.clone()); + PermissionState::Granted + } + PromptResponse::Deny => { + self.insert_prompt_denied(desc.clone()); + PermissionState::Denied + } + PromptResponse::AllowAll => { + self.insert_granted(None); + PermissionState::Granted + } + } + } + + fn revoke_desc(&mut self, desc: &Option) -> PermissionState { + match desc.as_ref() { + Some(desc) => self.granted_list.retain(|v| !v.stronger_than(desc)), + None => { + self.granted_global = false; + // Revoke global is a special case where the entire granted list is + // cleared. It's inconsistent with the granular case where only + // descriptors stronger than the revoked one are purged. + self.granted_list.clear(); + } + } + self.query_desc(desc, AllowPartial::TreatAsPartialGranted) + } + + fn is_granted(&self, desc: &Option) -> bool { + Self::list_contains(desc, self.granted_global, &self.granted_list) + } + + fn is_flag_denied(&self, desc: &Option) -> bool { + Self::list_contains(desc, self.flag_denied_global, &self.flag_denied_list) + } + + fn is_prompt_denied(&self, desc: &Option) -> bool { + match desc.as_ref() { + Some(desc) => self + .prompt_denied_list + .iter() + .any(|v| desc.stronger_than(v)), + None => self.prompt_denied_global || !self.prompt_denied_list.is_empty(), + } + } + + fn is_partial_flag_denied(&self, desc: &Option) -> bool { + match desc { + None => !self.flag_denied_list.is_empty(), + Some(desc) => self.flag_denied_list.iter().any(|v| desc.stronger_than(v)), + } + } + + fn list_contains( + desc: &Option, + list_global: bool, + list: &HashSet, + ) -> bool { + match desc.as_ref() { + Some(desc) => list_global || list.iter().any(|v| v.stronger_than(desc)), + None => list_global, + } + } + + fn insert_granted(&mut self, desc: Option) { + Self::list_insert(desc, &mut self.granted_global, &mut self.granted_list); + } + + fn insert_prompt_denied(&mut self, desc: Option) { + Self::list_insert( + desc, + &mut self.prompt_denied_global, + &mut self.prompt_denied_list, + ); + } + + fn list_insert( + desc: Option, + list_global: &mut bool, + list: &mut HashSet, + ) { + match desc { + Some(desc) => { + list.insert(desc); + } + None => *list_global = true, + } + } +} + #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct ReadDescriptor(pub PathBuf); +impl Descriptor for ReadDescriptor { + fn flag_name() -> &'static str { + "read" + } + + fn name(&self) -> Cow { + Cow::from(self.0.display().to_string()) + } + + fn stronger_than(&self, other: &Self) -> bool { + other.0.starts_with(&self.0) + } +} + #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct WriteDescriptor(pub PathBuf); +impl Descriptor for WriteDescriptor { + fn flag_name() -> &'static str { + "write" + } + + fn name(&self) -> Cow { + Cow::from(self.0.display().to_string()) + } + + fn stronger_than(&self, other: &Self) -> bool { + other.0.starts_with(&self.0) + } +} + #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct NetDescriptor(pub String, pub Option); @@ -251,6 +519,20 @@ impl NetDescriptor { } } +impl Descriptor for NetDescriptor { + fn flag_name() -> &'static str { + "net" + } + + fn name(&self) -> Cow { + Cow::from(format!("{}", self)) + } + + fn stronger_than(&self, other: &Self) -> bool { + self.0 == other.0 && (self.1.is_none() || self.1 == other.1) + } +} + impl FromStr for NetDescriptor { type Err = AnyError; @@ -280,6 +562,16 @@ impl EnvDescriptor { } } +impl Descriptor for EnvDescriptor { + fn flag_name() -> &'static str { + "env" + } + + fn name(&self) -> Cow { + Cow::from(self.0.as_ref()) + } +} + impl AsRef for EnvDescriptor { fn as_ref(&self) -> &str { self.0.as_ref() @@ -292,6 +584,16 @@ pub enum RunDescriptor { Path(PathBuf), } +impl Descriptor for RunDescriptor { + fn flag_name() -> &'static str { + "run" + } + + fn name(&self) -> Cow { + Cow::from(self.to_string()) + } +} + impl FromStr for RunDescriptor { type Err = (); @@ -319,6 +621,16 @@ impl ToString for RunDescriptor { #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct SysDescriptor(pub String); +impl Descriptor for SysDescriptor { + fn flag_name() -> &'static str { + "sys" + } + + fn name(&self) -> Cow { + Cow::from(self.0.to_string()) + } +} + pub fn parse_sys_kind(kind: &str) -> Result<&str, AnyError> { match kind { "hostname" | "osRelease" | "osUptime" | "loadavg" | "networkInterfaces" @@ -330,135 +642,63 @@ pub fn parse_sys_kind(kind: &str) -> Result<&str, AnyError> { #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct FfiDescriptor(pub PathBuf); +impl Descriptor for FfiDescriptor { + fn flag_name() -> &'static str { + "ffi" + } + + fn name(&self) -> Cow { + Cow::from(self.0.display().to_string()) + } + + fn stronger_than(&self, other: &Self) -> bool { + other.0.starts_with(&self.0) + } +} + impl UnaryPermission { pub fn query(&self, path: Option<&Path>) -> PermissionState { - if self.global_state == PermissionState::Granted { - return PermissionState::Granted; - } - let path = path.map(|p| resolve_from_cwd(p).unwrap()); - if self.global_state == PermissionState::Denied - && match path.as_ref() { - None => true, - Some(path) => self - .denied_list - .iter() - .any(|path_| path_.0.starts_with(path)), - } - { - PermissionState::Denied - } else if self.global_state == PermissionState::Granted - || match path.as_ref() { - None => false, - Some(path) => self - .granted_list - .iter() - .any(|path_| path.starts_with(&path_.0)), - } - { - PermissionState::Granted - } else { - PermissionState::Prompt - } + self.query_desc( + &path.map(|p| ReadDescriptor(resolve_from_cwd(p).unwrap())), + AllowPartial::TreatAsPartialGranted, + ) } pub fn request(&mut self, path: Option<&Path>) -> PermissionState { - if let Some(path) = path { - let (resolved_path, display_path) = resolved_and_display_path(path); - let state = self.query(Some(&resolved_path)); - if state == PermissionState::Prompt { - 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)); - PermissionState::Granted - } else { - state - } - } else { - let state = self.query(None); - if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - "read access", - self.name, - Some("Deno.permissions.query()"), - true, - ) - { - self.granted_list.clear(); - self.global_state = PermissionState::Granted; - PermissionState::Granted - } else { - self.global_state = PermissionState::Denied; - PermissionState::Denied - } - } else { - state - } - } + self.request_desc( + &path.map(|p| ReadDescriptor(resolve_from_cwd(p).unwrap())), + || Some(path?.display().to_string()), + ) } pub fn revoke(&mut self, path: Option<&Path>) -> PermissionState { - if let Some(path) = path { - let path = resolve_from_cwd(path).unwrap(); - self - .granted_list - .retain(|path_| !path.starts_with(&path_.0)); - } else { - self.granted_list.clear(); - } - if self.global_state == PermissionState::Granted { - self.global_state = PermissionState::Prompt; - } - self.query(path) + self + .revoke_desc(&path.map(|p| ReadDescriptor(resolve_from_cwd(p).unwrap()))) } - #[inline] pub fn check( &mut self, path: &Path, api_name: Option<&str>, ) -> Result<(), AnyError> { - let (result, prompted, is_allow_all) = self.query(Some(path)).check2( - self.name, + self.check_desc( + &Some(ReadDescriptor(resolve_from_cwd(path)?)), + true, api_name, - || Some(format!("\"{}\"", path.to_path_buf().display())), - self.prompt, - ); - if prompted { - let resolved_path = resolve_from_cwd(path)?; - if result.is_ok() { - 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; - } - } - result + || Some(format!("\"{}\"", path.display())), + ) + } + + #[inline] + pub fn check_partial( + &mut self, + path: &Path, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + let desc = ReadDescriptor(resolve_from_cwd(path)?); + self.check_desc(&Some(desc), false, api_name, || { + Some(format!("\"{}\"", path.display())) + }) } /// As `check()`, but permission error messages will anonymize the path @@ -469,205 +709,62 @@ impl UnaryPermission { display: &str, api_name: &str, ) -> Result<(), AnyError> { - let resolved_path = resolve_from_cwd(path)?; - 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() { - if is_allow_all { - self.granted_list.clear(); - self.global_state = PermissionState::Granted; - } else { - self.granted_list.insert(ReadDescriptor(resolved_path)); - } - } else { - self.global_state = PermissionState::Denied; - if !is_allow_all { - self.denied_list.insert(ReadDescriptor(resolved_path)); - } - } - } - result + let desc = ReadDescriptor(resolve_from_cwd(path)?); + self.check_desc(&Some(desc), false, Some(api_name), || { + Some(format!("<{display}>")) + }) } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { - let (result, prompted, _) = - self - .query(None) - .check(self.name, api_name, 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: "read", - description: "read the file system", - global_state: Default::default(), - granted_list: Default::default(), - denied_list: Default::default(), - prompt: false, - } + self.check_desc(&None, false, api_name, || None) } } impl UnaryPermission { pub fn query(&self, path: Option<&Path>) -> PermissionState { - if self.global_state == PermissionState::Granted { - return PermissionState::Granted; - } - let path = path.map(|p| resolve_from_cwd(p).unwrap()); - if self.global_state == PermissionState::Denied - && match path.as_ref() { - None => true, - Some(path) => self - .denied_list - .iter() - .any(|path_| path_.0.starts_with(path)), - } - { - PermissionState::Denied - } else if self.global_state == PermissionState::Granted - || match path.as_ref() { - None => false, - Some(path) => self - .granted_list - .iter() - .any(|path_| path.starts_with(&path_.0)), - } - { - PermissionState::Granted - } else { - PermissionState::Prompt - } + self.query_desc( + &path.map(|p| WriteDescriptor(resolve_from_cwd(p).unwrap())), + AllowPartial::TreatAsPartialGranted, + ) } pub fn request(&mut self, path: Option<&Path>) -> PermissionState { - if let Some(path) = path { - let (resolved_path, display_path) = resolved_and_display_path(path); - let state = self.query(Some(&resolved_path)); - if state == PermissionState::Prompt { - 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)); - PermissionState::Granted - } else { - state - } - } else { - let state = self.query(None); - if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - "write access", - self.name, - Some("Deno.permissions.query()"), - true, - ) - { - self.granted_list.clear(); - self.global_state = PermissionState::Granted; - PermissionState::Granted - } else { - self.global_state = PermissionState::Denied; - PermissionState::Denied - } - } else { - state - } - } + self.request_desc( + &path.map(|p| WriteDescriptor(resolve_from_cwd(p).unwrap())), + || Some(path?.display().to_string()), + ) } pub fn revoke(&mut self, path: Option<&Path>) -> PermissionState { - if let Some(path) = path { - let path = resolve_from_cwd(path).unwrap(); - self - .granted_list - .retain(|path_| !path.starts_with(&path_.0)); - } else { - self.granted_list.clear(); - } - if self.global_state == PermissionState::Granted { - self.global_state = PermissionState::Prompt; - } - self.query(path) + self + .revoke_desc(&path.map(|p| WriteDescriptor(resolve_from_cwd(p).unwrap()))) } - #[inline] pub fn check( &mut self, path: &Path, api_name: Option<&str>, ) -> Result<(), AnyError> { - let (result, prompted, is_allow_all) = self.query(Some(path)).check2( - self.name, + self.check_desc( + &Some(WriteDescriptor(resolve_from_cwd(path)?)), + true, api_name, - || Some(format!("\"{}\"", path.to_path_buf().display())), - self.prompt, - ); - if prompted { - let resolved_path = resolve_from_cwd(path)?; - if result.is_ok() { - 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; - } - } - result + || Some(format!("\"{}\"", path.display())), + ) } - pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { - let (result, prompted, _) = - self - .query(None) - .check(self.name, api_name, Some("all"), self.prompt); - if prompted { - if result.is_ok() { - self.global_state = PermissionState::Granted; - } else { - self.global_state = PermissionState::Denied; - } - } - result + #[inline] + pub fn check_partial( + &mut self, + path: &Path, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + self.check_desc( + &Some(WriteDescriptor(resolve_from_cwd(path)?)), + false, + api_name, + || Some(format!("\"{}\"", path.display())), + ) } /// As `check()`, but permission error messages will anonymize the path @@ -678,43 +775,14 @@ impl UnaryPermission { display: &str, api_name: &str, ) -> Result<(), AnyError> { - let resolved_path = resolve_from_cwd(path)?; - 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() { - if is_allow_all { - self.granted_list.clear(); - self.global_state = PermissionState::Granted; - } else { - self.granted_list.insert(WriteDescriptor(resolved_path)); - } - } else { - self.global_state = PermissionState::Denied; - if !is_allow_all { - self.denied_list.insert(WriteDescriptor(resolved_path)); - } - } - } - result + let desc = WriteDescriptor(resolve_from_cwd(path)?); + self.check_desc(&Some(desc), false, Some(api_name), || { + Some(format!("<{display}>")) + }) } -} -impl Default for UnaryPermission { - fn default() -> Self { - UnaryPermission:: { - name: "write", - description: "write to the file system", - global_state: Default::default(), - granted_list: Default::default(), - denied_list: Default::default(), - prompt: false, - } + pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { + self.check_desc(&None, false, api_name, || None) } } @@ -723,116 +791,24 @@ impl UnaryPermission { &self, host: Option<&(T, Option)>, ) -> PermissionState { - if self.global_state == PermissionState::Denied - && match host.as_ref() { - None => true, - Some(host) => match host.1 { - None => self - .denied_list - .iter() - .any(|host_| host.0.as_ref() == host_.0), - Some(_) => self.denied_list.contains(&NetDescriptor::new(host)), - }, - } - { - PermissionState::Denied - } else if self.global_state == PermissionState::Granted - || match host.as_ref() { - None => false, - Some(host) => { - self.granted_list.contains(&NetDescriptor::new(&&( - host.0.as_ref().to_string(), - None, - ))) - || self.granted_list.contains(&NetDescriptor::new(host)) - } - } - { - PermissionState::Granted - } else { - PermissionState::Prompt - } + self.query_desc( + &host.map(|h| NetDescriptor::new(&h)), + AllowPartial::TreatAsPartialGranted, + ) } pub fn request>( &mut self, host: Option<&(T, Option)>, ) -> PermissionState { - if let Some(host) = host { - let state = self.query(Some(host)); - let host = NetDescriptor::new(&host); - if state == PermissionState::Prompt { - 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); - PermissionState::Granted - } else { - state - } - } else { - let state = self.query::<&str>(None); - if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - "network access", - self.name, - Some("Deno.permissions.query()"), - true, - ) - { - self.granted_list.clear(); - self.global_state = PermissionState::Granted; - PermissionState::Granted - } else { - self.global_state = PermissionState::Denied; - PermissionState::Denied - } - } else { - state - } - } + self.request_desc(&host.map(|h| NetDescriptor::new(&h)), || None) } pub fn revoke>( &mut self, host: Option<&(T, Option)>, ) -> PermissionState { - if let Some(host) = host { - if host.1.is_some() { - self - .granted_list - .remove(&NetDescriptor(host.0.as_ref().to_string(), host.1)); - } - self - .granted_list - .remove(&NetDescriptor(host.0.as_ref().to_string(), None)); - } else { - self.granted_list.clear(); - } - if self.global_state == PermissionState::Granted { - self.global_state = PermissionState::Prompt; - } - self.query(host) + self.revoke_desc(&Some(NetDescriptor::new(&host.unwrap()))) } pub fn check>( @@ -840,27 +816,7 @@ impl UnaryPermission { host: &(T, Option), api_name: Option<&str>, ) -> Result<(), AnyError> { - let new_host = NetDescriptor::new(&host); - let (result, prompted, is_allow_all) = self.query(Some(host)).check( - self.name, - api_name, - Some(&format!("\"{new_host}\"")), - self.prompt, - ); - if prompted { - if result.is_ok() { - 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; - } - } - result + self.check_desc(&Some(NetDescriptor::new(&host)), false, api_name, || None) } pub fn check_url( @@ -872,284 +828,60 @@ impl UnaryPermission { .host_str() .ok_or_else(|| uri_error("Missing host"))? .to_string(); - let display_host = match url.port() { - None => Cow::Borrowed(&hostname), - Some(port) => Cow::Owned(format!("{hostname}:{port}")), - }; let host = &(&hostname, url.port_or_known_default()); - let (result, prompted, is_allow_all) = self.query(Some(host)).check( - self.name, - api_name, - Some(&format!("\"{display_host}\"")), - self.prompt, - ); - if prompted { - if result.is_ok() { - 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; - } - } - result + let display_host = match url.port() { + None => hostname.clone(), + Some(port) => format!("{hostname}:{port}"), + }; + self.check_desc(&Some(NetDescriptor::new(&host)), false, api_name, || { + Some(format!("\"{}\"", display_host)) + }) } pub fn check_all(&mut self) -> Result<(), AnyError> { - let (result, prompted, _) = - self - .query::<&str>(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: "net", - description: "network", - global_state: Default::default(), - granted_list: Default::default(), - denied_list: Default::default(), - prompt: false, - } + self.check_desc(&None, false, None, || None) } } impl UnaryPermission { pub fn query(&self, env: Option<&str>) -> PermissionState { - let env = env.map(EnvVarName::new); - if self.global_state == PermissionState::Denied - && match env.as_ref() { - None => true, - Some(env) => self.denied_list.contains(&EnvDescriptor::new(env)), - } - { - PermissionState::Denied - } else if self.global_state == PermissionState::Granted - || match env.as_ref() { - None => false, - Some(env) => self.granted_list.contains(&EnvDescriptor::new(env)), - } - { - PermissionState::Granted - } else { - PermissionState::Prompt - } + self.query_desc( + &env.map(EnvDescriptor::new), + AllowPartial::TreatAsPartialGranted, + ) } pub fn request(&mut self, env: Option<&str>) -> PermissionState { - if let Some(env) = env { - let state = self.query(Some(env)); - if state == PermissionState::Prompt { - 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)); - PermissionState::Granted - } else { - state - } - } else { - let state = self.query(None); - if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - "env access", - self.name, - Some("Deno.permissions.query()"), - true, - ) - { - self.granted_list.clear(); - self.global_state = PermissionState::Granted; - PermissionState::Granted - } else { - self.global_state = PermissionState::Denied; - PermissionState::Denied - } - } else { - state - } - } + self.request_desc(&env.map(EnvDescriptor::new), || None) } pub fn revoke(&mut self, env: Option<&str>) -> PermissionState { - if let Some(env) = env { - self.granted_list.remove(&EnvDescriptor::new(env)); - } else { - self.granted_list.clear(); - } - if self.global_state == PermissionState::Granted { - self.global_state = PermissionState::Prompt; - } - self.query(env) + self.revoke_desc(&env.map(EnvDescriptor::new)) } pub fn check(&mut self, env: &str) -> Result<(), AnyError> { - let (result, prompted, is_allow_all) = self.query(Some(env)).check( - self.name, - None, - Some(&format!("\"{env}\"")), - self.prompt, - ); - if prompted { - if result.is_ok() { - 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; - } - } - result + self.check_desc(&Some(EnvDescriptor::new(env)), false, None, || None) } 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: "env", - description: "environment variables", - global_state: Default::default(), - granted_list: Default::default(), - denied_list: Default::default(), - prompt: false, - } + self.check_desc(&None, false, None, || None) } } 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 - } + self.query_desc( + &kind.map(|k| SysDescriptor(k.to_string())), + AllowPartial::TreatAsPartialGranted, + ) } 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()); - 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 - == permission_prompt( - "sys access", - self.name, - Some("Deno.permissions.query()"), - true, - ) - { - self.global_state = PermissionState::Granted; - } else { - self.granted_list.clear(); - self.global_state = PermissionState::Denied; - } - self.global_state - } + self.request_desc(&kind.map(|k| SysDescriptor(k.to_string())), || None) } 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) + self.revoke_desc(&kind.map(|k| SysDescriptor(k.to_string()))) } pub fn check( @@ -1157,155 +889,35 @@ impl UnaryPermission { kind: &str, api_name: Option<&str>, ) -> Result<(), AnyError> { - let (result, prompted, is_allow_all) = self.query(Some(kind)).check( - self.name, + self.check_desc( + &Some(SysDescriptor(kind.to_string())), + false, api_name, - Some(&format!("\"{kind}\"")), - self.prompt, - ); - if prompted { - if result.is_ok() { - 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; - } - } - result + || None, + ) } pub fn check_all(&mut self) -> Result<(), AnyError> { - let (result, prompted, _is_allow_all) = - 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, - } + self.check_desc(&None, false, None, || None) } } 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 - .contains(&RunDescriptor::from_str(cmd).unwrap()), - } - { - PermissionState::Denied - } else if self.global_state == PermissionState::Granted - || match cmd { - None => false, - Some(cmd) => self - .granted_list - .contains(&RunDescriptor::from_str(cmd).unwrap()), - } - { - PermissionState::Granted - } else { - PermissionState::Prompt - } + self.query_desc( + &cmd.map(|c| RunDescriptor::from_str(c).unwrap()), + AllowPartial::TreatAsPartialGranted, + ) } pub fn request(&mut self, cmd: Option<&str>) -> PermissionState { - if let Some(cmd) = cmd { - let state = self.query(Some(cmd)); - if state == PermissionState::Prompt { - 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 - .granted_list - .insert(RunDescriptor::from_str(cmd).unwrap()); - PermissionState::Granted - } else { - state - } - } else { - let state = self.query(None); - if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - "run access", - self.name, - Some("Deno.permissions.query()"), - true, - ) - { - self.granted_list.clear(); - self.global_state = PermissionState::Granted; - PermissionState::Granted - } else { - self.global_state = PermissionState::Denied; - PermissionState::Denied - } - } else { - state - } - } + self.request_desc(&cmd.map(|c| RunDescriptor::from_str(c).unwrap()), || { + Some(cmd?.to_string()) + }) } pub fn revoke(&mut self, cmd: Option<&str>) -> PermissionState { - if let Some(cmd) = cmd { - self - .granted_list - .remove(&RunDescriptor::from_str(cmd).unwrap()); - } else { - self.granted_list.clear(); - } - if self.global_state == PermissionState::Granted { - self.global_state = PermissionState::Prompt; - } - self.query(cmd) + self.revoke_desc(&cmd.map(|c| RunDescriptor::from_str(c).unwrap())) } pub fn check( @@ -1313,228 +925,63 @@ impl UnaryPermission { cmd: &str, api_name: Option<&str>, ) -> Result<(), AnyError> { - let (result, prompted, is_allow_all) = self.query(Some(cmd)).check( - self.name, + self.check_desc( + &Some(RunDescriptor::from_str(cmd).unwrap()), + false, api_name, - Some(&format!("\"{cmd}\"")), - self.prompt, - ); - if prompted { - if result.is_ok() { - 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 - .insert(RunDescriptor::from_str(cmd).unwrap()); - self.global_state = PermissionState::Denied; - } - } - result + || Some(format!("\"{}\"", cmd)), + ) } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { - let (result, prompted, _) = - self - .query(None) - .check(self.name, api_name, 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: "run", - description: "run a subprocess", - global_state: Default::default(), - granted_list: Default::default(), - denied_list: Default::default(), - prompt: false, - } + self.check_desc(&None, false, api_name, || None) } } impl UnaryPermission { pub fn query(&self, path: Option<&Path>) -> PermissionState { - let path = path.map(|p| resolve_from_cwd(p).unwrap()); - if self.global_state == PermissionState::Denied - && match path.as_ref() { - None => true, - Some(path) => self - .denied_list - .iter() - .any(|path_| path_.0.starts_with(path)), - } - { - PermissionState::Denied - } else if self.global_state == PermissionState::Granted - || match path.as_ref() { - None => false, - Some(path) => self - .granted_list - .iter() - .any(|path_| path.starts_with(&path_.0)), - } - { - PermissionState::Granted - } else { - PermissionState::Prompt - } + self.query_desc( + &path.map(|p| FfiDescriptor(resolve_from_cwd(p).unwrap())), + AllowPartial::TreatAsPartialGranted, + ) } pub fn request(&mut self, path: Option<&Path>) -> PermissionState { - if let Some(path) = path { - let (resolved_path, display_path) = resolved_and_display_path(path); - let state = self.query(Some(&resolved_path)); - if state == PermissionState::Prompt { - 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)); - PermissionState::Granted - } else { - state - } - } else { - let state = self.query(None); - if state == PermissionState::Prompt { - if PromptResponse::Allow - == permission_prompt( - "ffi access", - self.name, - Some("Deno.permissions.query()"), - true, - ) - { - self.granted_list.clear(); - self.global_state = PermissionState::Granted; - PermissionState::Granted - } else { - self.global_state = PermissionState::Denied; - PermissionState::Denied - } - } else { - state - } - } + self.request_desc( + &path.map(|p| FfiDescriptor(resolve_from_cwd(p).unwrap())), + || Some(path?.display().to_string()), + ) } pub fn revoke(&mut self, path: Option<&Path>) -> PermissionState { - if let Some(path) = path { - let path = resolve_from_cwd(path).unwrap(); - self - .granted_list - .retain(|path_| !path.starts_with(&path_.0)); - } else { - self.granted_list.clear(); - } - if self.global_state == PermissionState::Granted { - self.global_state = PermissionState::Prompt; - } - self.query(path) + self.revoke_desc(&path.map(|p| FfiDescriptor(resolve_from_cwd(p).unwrap()))) } - 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, is_allow_all) = - self.query(Some(&resolved_path)).check( - self.name, - None, - Some(&format!("\"{}\"", display_path.display())), - self.prompt, - ); + pub fn check( + &mut self, + path: &Path, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + self.check_desc( + &Some(FfiDescriptor(resolve_from_cwd(path)?)), + true, + api_name, + || Some(format!("\"{}\"", path.display())), + ) + } - if prompted { - if result.is_ok() { - 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; - } - } - - result - } else { - let (result, prompted, _) = - self.query(None).check(self.name, None, None, self.prompt); - - if prompted { - if result.is_ok() { - self.global_state = PermissionState::Granted; - } else { - self.global_state = PermissionState::Denied; - } - } - - result - } + pub fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError> { + let desc = match path { + Some(path) => Some(FfiDescriptor(resolve_from_cwd(path)?)), + None => None, + }; + self.check_desc(&desc, false, None, || { + Some(format!("\"{}\"", path?.display())) + }) } 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: "ffi", - description: "load a dynamic library", - global_state: Default::default(), - granted_list: Default::default(), - denied_list: Default::default(), - prompt: false, - } + self.check_desc(&None, false, Some("all"), || None) } } @@ -1553,14 +1000,14 @@ pub struct Permissions { impl Default for Permissions { fn default() -> Self { Self { - read: Permissions::new_read(&None, false).unwrap(), - 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), + read: Permissions::new_read(&None, &None, false).unwrap(), + write: Permissions::new_write(&None, &None, false).unwrap(), + net: Permissions::new_net(&None, &None, false).unwrap(), + env: Permissions::new_env(&None, &None, false).unwrap(), + sys: Permissions::new_sys(&None, &None, false).unwrap(), + run: Permissions::new_run(&None, &None, false).unwrap(), + ffi: Permissions::new_ffi(&None, &None, false).unwrap(), + hrtime: Permissions::new_hrtime(false, false), } } } @@ -1568,150 +1015,134 @@ impl Default for Permissions { #[derive(Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize)] pub struct PermissionsOptions { pub allow_env: Option>, + pub deny_env: Option>, pub allow_hrtime: bool, + pub deny_hrtime: bool, pub allow_net: Option>, + pub deny_net: Option>, pub allow_ffi: Option>, + pub deny_ffi: Option>, pub allow_read: Option>, + pub deny_read: Option>, pub allow_run: Option>, + pub deny_run: Option>, pub allow_sys: Option>, + pub deny_sys: Option>, pub allow_write: Option>, + pub deny_write: Option>, pub prompt: bool, } impl Permissions { pub fn new_read( - state: &Option>, + allow_list: &Option>, + deny_list: &Option>, prompt: bool, ) -> Result, AnyError> { Ok(UnaryPermission:: { - global_state: global_state_from_option(state), - granted_list: resolve_read_allowlist(state)?, + granted_global: global_from_option(allow_list), + granted_list: parse_path_list(allow_list, ReadDescriptor)?, + flag_denied_global: global_from_option(deny_list), + flag_denied_list: parse_path_list(deny_list, ReadDescriptor)?, prompt, ..Default::default() }) } pub fn new_write( - state: &Option>, + allow_list: &Option>, + deny_list: &Option>, prompt: bool, ) -> Result, AnyError> { - Ok(UnaryPermission:: { - global_state: global_state_from_option(state), - granted_list: resolve_write_allowlist(state)?, + Ok(UnaryPermission { + granted_global: global_from_option(allow_list), + granted_list: parse_path_list(allow_list, WriteDescriptor)?, + flag_denied_global: global_from_option(deny_list), + flag_denied_list: parse_path_list(deny_list, WriteDescriptor)?, prompt, ..Default::default() }) } pub fn new_net( - state: &Option>, + allow_list: &Option>, + deny_list: &Option>, prompt: bool, ) -> Result, AnyError> { Ok(UnaryPermission:: { - global_state: global_state_from_option(state), - granted_list: state - .as_ref() - .map(|v| { - v.iter() - .map(|x| NetDescriptor::from_str(x)) - .collect::, AnyError>>() - }) - .unwrap_or_else(|| Ok(HashSet::new()))?, + granted_global: global_from_option(allow_list), + granted_list: parse_net_list(allow_list)?, + flag_denied_global: global_from_option(deny_list), + flag_denied_list: parse_net_list(deny_list)?, prompt, ..Default::default() }) } pub fn new_env( - state: &Option>, + allow_list: &Option>, + deny_list: &Option>, prompt: bool, ) -> Result, AnyError> { Ok(UnaryPermission:: { - global_state: global_state_from_option(state), - granted_list: state - .as_ref() - .map(|v| { - v.iter() - .map(|x| { - if x.is_empty() { - Err(AnyError::msg("Empty path is not allowed")) - } else { - Ok(EnvDescriptor::new(x)) - } - }) - .collect() - }) - .unwrap_or_else(|| Ok(HashSet::new()))?, + granted_global: global_from_option(allow_list), + granted_list: parse_env_list(allow_list)?, + flag_denied_global: global_from_option(deny_list), + flag_denied_list: parse_env_list(deny_list)?, prompt, ..Default::default() }) } pub fn new_sys( - state: &Option>, + allow_list: &Option>, + deny_list: &Option>, prompt: bool, ) -> Result, AnyError> { Ok(UnaryPermission:: { - global_state: global_state_from_option(state), - granted_list: state - .as_ref() - .map(|v| { - v.iter() - .map(|x| { - if x.is_empty() { - Err(AnyError::msg("empty")) - } else { - Ok(SysDescriptor(x.to_string())) - } - }) - .collect() - }) - .unwrap_or_else(|| Ok(HashSet::new()))?, + granted_global: global_from_option(allow_list), + granted_list: parse_sys_list(allow_list)?, + flag_denied_global: global_from_option(deny_list), + flag_denied_list: parse_sys_list(deny_list)?, prompt, ..Default::default() }) } pub fn new_run( - state: &Option>, + allow_list: &Option>, + deny_list: &Option>, prompt: bool, ) -> Result, AnyError> { Ok(UnaryPermission:: { - global_state: global_state_from_option(state), - granted_list: state - .as_ref() - .map(|v| { - v.iter() - .map(|x| { - if x.is_empty() { - Err(AnyError::msg("Empty path is not allowed")) - } else { - Ok(RunDescriptor::from_str(x).unwrap()) - } - }) - .collect() - }) - .unwrap_or_else(|| Ok(HashSet::new()))?, + granted_global: global_from_option(allow_list), + granted_list: parse_run_list(allow_list)?, + flag_denied_global: global_from_option(deny_list), + flag_denied_list: parse_run_list(deny_list)?, prompt, ..Default::default() }) } pub fn new_ffi( - state: &Option>, + allow_list: &Option>, + deny_list: &Option>, prompt: bool, ) -> Result, AnyError> { Ok(UnaryPermission:: { - global_state: global_state_from_option(state), - granted_list: resolve_ffi_allowlist(state)?, + granted_global: global_from_option(allow_list), + granted_list: parse_path_list(allow_list, FfiDescriptor)?, + flag_denied_global: global_from_option(deny_list), + flag_denied_list: parse_path_list(deny_list, FfiDescriptor)?, prompt, ..Default::default() }) } - pub fn new_hrtime(state: bool) -> UnitPermission { - unit_permission_from_flag_bool( - state, + pub fn new_hrtime(allow_state: bool, deny_state: bool) -> UnitPermission { + unit_permission_from_flag_bools( + allow_state, + deny_state, "hrtime", "high precision time", false, // never prompt for hrtime @@ -1720,27 +1151,35 @@ impl Permissions { pub fn from_options(opts: &PermissionsOptions) -> Result { Ok(Self { - read: Permissions::new_read(&opts.allow_read, opts.prompt)?, - 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), + read: Permissions::new_read( + &opts.allow_read, + &opts.deny_read, + opts.prompt, + )?, + write: Permissions::new_write( + &opts.allow_write, + &opts.deny_write, + opts.prompt, + )?, + net: Permissions::new_net(&opts.allow_net, &opts.deny_net, opts.prompt)?, + env: Permissions::new_env(&opts.allow_env, &opts.deny_env, opts.prompt)?, + sys: Permissions::new_sys(&opts.allow_sys, &opts.deny_sys, opts.prompt)?, + run: Permissions::new_run(&opts.allow_run, &opts.deny_run, opts.prompt)?, + ffi: Permissions::new_ffi(&opts.allow_ffi, &opts.deny_ffi, opts.prompt)?, + hrtime: Permissions::new_hrtime(opts.allow_hrtime, opts.deny_hrtime), }) } pub fn allow_all() -> Self { Self { - read: Permissions::new_read(&Some(vec![]), false).unwrap(), - 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), + read: Permissions::new_read(&Some(vec![]), &None, false).unwrap(), + write: Permissions::new_write(&Some(vec![]), &None, false).unwrap(), + net: Permissions::new_net(&Some(vec![]), &None, false).unwrap(), + env: Permissions::new_env(&Some(vec![]), &None, false).unwrap(), + sys: Permissions::new_sys(&Some(vec![]), &None, false).unwrap(), + run: Permissions::new_run(&Some(vec![]), &None, false).unwrap(), + ffi: Permissions::new_ffi(&Some(vec![]), &None, false).unwrap(), + hrtime: Permissions::new_hrtime(true, false), } } @@ -1990,6 +1429,14 @@ impl deno_fs::FsPermissions for PermissionsContainer { self.0.lock().write.check(path, Some(api_name)) } + fn check_write_partial( + &mut self, + path: &Path, + api_name: &str, + ) -> Result<(), AnyError> { + self.0.lock().write.check_partial(path, Some(api_name)) + } + fn check_write_blind( &mut self, p: &Path, @@ -2013,14 +1460,14 @@ impl deno_fs::FsPermissions for PermissionsContainer { impl deno_napi::NapiPermissions for PermissionsContainer { #[inline(always)] fn check(&mut self, path: Option<&Path>) -> Result<(), AnyError> { - self.0.lock().ffi.check(path) + self.0.lock().ffi.check(path.unwrap(), None) } } impl deno_ffi::FfiPermissions for PermissionsContainer { #[inline(always)] - fn check(&mut self, path: Option<&Path>) -> Result<(), AnyError> { - self.0.lock().ffi.check(path) + fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError> { + self.0.lock().ffi.check_partial(path) } } @@ -2036,8 +1483,9 @@ impl deno_kv::sqlite::SqliteDbHandlerPermissions for PermissionsContainer { } } -fn unit_permission_from_flag_bool( - flag: bool, +fn unit_permission_from_flag_bools( + allow_flag: bool, + deny_flag: bool, name: &'static str, description: &'static str, prompt: bool, @@ -2045,7 +1493,9 @@ fn unit_permission_from_flag_bool( UnitPermission { name, description, - state: if flag { + state: if deny_flag { + PermissionState::Denied + } else if allow_flag { PermissionState::Granted } else { PermissionState::Prompt @@ -2054,24 +1504,32 @@ fn unit_permission_from_flag_bool( } } -fn global_state_from_option(flag: &Option>) -> PermissionState { - if matches!(flag, Some(v) if v.is_empty()) { - PermissionState::Granted +fn global_from_option(flag: &Option>) -> bool { + matches!(flag, Some(v) if v.is_empty()) +} + +fn parse_net_list( + list: &Option>, +) -> Result, AnyError> { + if let Some(v) = list { + v.iter() + .map(|x| NetDescriptor::from_str(x)) + .collect::, AnyError>>() } else { - PermissionState::Prompt + Ok(HashSet::new()) } } -pub fn resolve_read_allowlist( - allow: &Option>, -) -> Result, AnyError> { - if let Some(v) = allow { +fn parse_env_list( + list: &Option>, +) -> Result, AnyError> { + if let Some(v) = list { v.iter() - .map(|raw_path| { - if raw_path.as_os_str().is_empty() { + .map(|x| { + if x.is_empty() { Err(AnyError::msg("Empty path is not allowed")) } else { - resolve_from_cwd(Path::new(&raw_path)).map(ReadDescriptor) + Ok(EnvDescriptor::new(x)) } }) .collect() @@ -2080,16 +1538,17 @@ pub fn resolve_read_allowlist( } } -pub fn resolve_write_allowlist( - allow: &Option>, -) -> Result, AnyError> { - if let Some(v) = allow { +fn parse_path_list( + list: &Option>, + f: fn(PathBuf) -> T, +) -> Result, AnyError> { + if let Some(v) = list { v.iter() .map(|raw_path| { if raw_path.as_os_str().is_empty() { Err(AnyError::msg("Empty path is not allowed")) } else { - resolve_from_cwd(Path::new(&raw_path)).map(WriteDescriptor) + resolve_from_cwd(Path::new(&raw_path)).map(f) } }) .collect() @@ -2098,16 +1557,16 @@ pub fn resolve_write_allowlist( } } -pub fn resolve_ffi_allowlist( - allow: &Option>, -) -> Result, AnyError> { - if let Some(v) = allow { +fn parse_sys_list( + list: &Option>, +) -> Result, AnyError> { + if let Some(v) = list { v.iter() - .map(|raw_path| { - if raw_path.as_os_str().is_empty() { - Err(AnyError::msg("Empty path is not allowed")) + .map(|x| { + if x.is_empty() { + Err(AnyError::msg("empty")) } else { - resolve_from_cwd(Path::new(&raw_path)).map(FfiDescriptor) + Ok(SysDescriptor(x.to_string())) } }) .collect() @@ -2116,13 +1575,22 @@ pub fn resolve_ffi_allowlist( } } -/// Arbitrary helper. Resolves the path from CWD, and also gets a path that -/// can be displayed without leaking the CWD when not allowed. -#[inline] -fn resolved_and_display_path(path: &Path) -> (PathBuf, PathBuf) { - let resolved_path = resolve_from_cwd(path).unwrap(); - let display_path = path.to_path_buf(); - (resolved_path, display_path) +fn parse_run_list( + list: &Option>, +) -> Result, AnyError> { + if let Some(v) = list { + v.iter() + .map(|x| { + if x.is_empty() { + Err(AnyError::msg("Empty path is not allowed")) + } else { + Ok(RunDescriptor::from_str(x).unwrap()) + } + }) + .collect() + } else { + Ok(HashSet::new()) + } } fn escalation_error() -> AnyError { @@ -2397,12 +1865,11 @@ pub fn create_child_permissions( if main_perms.env.check_all().is_err() { return Err(escalation_error()); } - worker_perms.env.global_state = PermissionState::Granted; + worker_perms.env.granted_global = true; } ChildUnaryPermissionArg::NotGranted => {} ChildUnaryPermissionArg::GrantedList(granted_list) => { - worker_perms.env.granted_list = - Permissions::new_env(&Some(granted_list), false)?.granted_list; + worker_perms.env.granted_list = parse_env_list(&Some(granted_list))?; if !worker_perms .env .granted_list @@ -2413,10 +1880,11 @@ pub fn create_child_permissions( } } } - worker_perms.env.denied_list = main_perms.env.denied_list.clone(); - if main_perms.env.global_state == PermissionState::Denied { - worker_perms.env.global_state = PermissionState::Denied; - } + worker_perms.env.flag_denied_global = main_perms.env.flag_denied_global; + worker_perms.env.flag_denied_list = main_perms.env.flag_denied_list.clone(); + worker_perms.env.prompt_denied_global = main_perms.env.prompt_denied_global; + worker_perms.env.prompt_denied_list = + main_perms.env.prompt_denied_list.clone(); worker_perms.env.prompt = main_perms.env.prompt; match child_permissions_arg.sys { ChildUnaryPermissionArg::Inherit => { @@ -2426,12 +1894,11 @@ pub fn create_child_permissions( if main_perms.sys.check_all().is_err() { return Err(escalation_error()); } - worker_perms.sys.global_state = PermissionState::Granted; + worker_perms.sys.granted_global = true; } ChildUnaryPermissionArg::NotGranted => {} ChildUnaryPermissionArg::GrantedList(granted_list) => { - worker_perms.sys.granted_list = - Permissions::new_sys(&Some(granted_list), false)?.granted_list; + worker_perms.sys.granted_list = parse_sys_list(&Some(granted_list))?; if !worker_perms .sys .granted_list @@ -2442,10 +1909,11 @@ pub fn create_child_permissions( } } } - 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.flag_denied_global = main_perms.sys.flag_denied_global; + worker_perms.sys.flag_denied_list = main_perms.sys.flag_denied_list.clone(); + worker_perms.sys.prompt_denied_global = main_perms.sys.prompt_denied_global; + worker_perms.sys.prompt_denied_list = + main_perms.sys.prompt_denied_list.clone(); worker_perms.sys.prompt = main_perms.sys.prompt; match child_permissions_arg.hrtime { ChildUnitPermissionArg::Inherit => { @@ -2471,12 +1939,11 @@ pub fn create_child_permissions( if main_perms.net.check_all().is_err() { return Err(escalation_error()); } - worker_perms.net.global_state = PermissionState::Granted; + worker_perms.net.granted_global = true; } ChildUnaryPermissionArg::NotGranted => {} ChildUnaryPermissionArg::GrantedList(granted_list) => { - worker_perms.net.granted_list = - Permissions::new_net(&Some(granted_list), false)?.granted_list; + worker_perms.net.granted_list = parse_net_list(&Some(granted_list))?; if !worker_perms .net .granted_list @@ -2487,10 +1954,11 @@ pub fn create_child_permissions( } } } - worker_perms.net.denied_list = main_perms.net.denied_list.clone(); - if main_perms.net.global_state == PermissionState::Denied { - worker_perms.net.global_state = PermissionState::Denied; - } + worker_perms.net.flag_denied_global = main_perms.net.flag_denied_global; + worker_perms.net.flag_denied_list = main_perms.net.flag_denied_list.clone(); + worker_perms.net.prompt_denied_global = main_perms.net.prompt_denied_global; + worker_perms.net.prompt_denied_list = + main_perms.net.prompt_denied_list.clone(); worker_perms.net.prompt = main_perms.net.prompt; match child_permissions_arg.ffi { ChildUnaryPermissionArg::Inherit => { @@ -2500,29 +1968,29 @@ pub fn create_child_permissions( if main_perms.ffi.check_all().is_err() { return Err(escalation_error()); } - worker_perms.ffi.global_state = PermissionState::Granted; + worker_perms.ffi.granted_global = true; } ChildUnaryPermissionArg::NotGranted => {} ChildUnaryPermissionArg::GrantedList(granted_list) => { - worker_perms.ffi.granted_list = Permissions::new_ffi( + worker_perms.ffi.granted_list = parse_path_list( &Some(granted_list.iter().map(PathBuf::from).collect()), - false, - )? - .granted_list; + FfiDescriptor, + )?; if !worker_perms .ffi .granted_list .iter() - .all(|desc| main_perms.ffi.check(Some(&desc.0)).is_ok()) + .all(|desc| main_perms.ffi.check(&desc.0, None).is_ok()) { return Err(escalation_error()); } } } - worker_perms.ffi.denied_list = main_perms.ffi.denied_list.clone(); - if main_perms.ffi.global_state == PermissionState::Denied { - worker_perms.ffi.global_state = PermissionState::Denied; - } + worker_perms.ffi.flag_denied_global = main_perms.env.flag_denied_global; + worker_perms.ffi.flag_denied_list = main_perms.ffi.flag_denied_list.clone(); + worker_perms.ffi.prompt_denied_global = main_perms.ffi.prompt_denied_global; + worker_perms.ffi.prompt_denied_list = + main_perms.ffi.prompt_denied_list.clone(); worker_perms.ffi.prompt = main_perms.ffi.prompt; match child_permissions_arg.read { ChildUnaryPermissionArg::Inherit => { @@ -2532,15 +2000,14 @@ pub fn create_child_permissions( if main_perms.read.check_all(None).is_err() { return Err(escalation_error()); } - worker_perms.read.global_state = PermissionState::Granted; + worker_perms.read.granted_global = true; } ChildUnaryPermissionArg::NotGranted => {} ChildUnaryPermissionArg::GrantedList(granted_list) => { - worker_perms.read.granted_list = Permissions::new_read( + worker_perms.read.granted_list = parse_path_list( &Some(granted_list.iter().map(PathBuf::from).collect()), - false, - )? - .granted_list; + ReadDescriptor, + )?; if !worker_perms .read .granted_list @@ -2551,10 +2018,11 @@ pub fn create_child_permissions( } } } - worker_perms.read.denied_list = main_perms.read.denied_list.clone(); - if main_perms.read.global_state == PermissionState::Denied { - worker_perms.read.global_state = PermissionState::Denied; - } + worker_perms.read.flag_denied_global = main_perms.read.flag_denied_global; + worker_perms.read.flag_denied_list = main_perms.read.flag_denied_list.clone(); + worker_perms.read.prompt_denied_global = main_perms.read.prompt_denied_global; + worker_perms.read.prompt_denied_list = + main_perms.read.prompt_denied_list.clone(); worker_perms.read.prompt = main_perms.read.prompt; match child_permissions_arg.run { ChildUnaryPermissionArg::Inherit => { @@ -2564,12 +2032,11 @@ pub fn create_child_permissions( if main_perms.run.check_all(None).is_err() { return Err(escalation_error()); } - worker_perms.run.global_state = PermissionState::Granted; + worker_perms.run.granted_global = true; } ChildUnaryPermissionArg::NotGranted => {} ChildUnaryPermissionArg::GrantedList(granted_list) => { - worker_perms.run.granted_list = - Permissions::new_run(&Some(granted_list), false)?.granted_list; + worker_perms.run.granted_list = parse_run_list(&Some(granted_list))?; if !worker_perms .run .granted_list @@ -2580,10 +2047,11 @@ pub fn create_child_permissions( } } } - worker_perms.run.denied_list = main_perms.run.denied_list.clone(); - if main_perms.run.global_state == PermissionState::Denied { - worker_perms.run.global_state = PermissionState::Denied; - } + worker_perms.run.flag_denied_global = main_perms.run.flag_denied_global; + worker_perms.run.flag_denied_list = main_perms.run.flag_denied_list.clone(); + worker_perms.run.prompt_denied_global = main_perms.run.prompt_denied_global; + worker_perms.run.prompt_denied_list = + main_perms.run.prompt_denied_list.clone(); worker_perms.run.prompt = main_perms.run.prompt; match child_permissions_arg.write { ChildUnaryPermissionArg::Inherit => { @@ -2593,15 +2061,14 @@ pub fn create_child_permissions( if main_perms.write.check_all(None).is_err() { return Err(escalation_error()); } - worker_perms.write.global_state = PermissionState::Granted; + worker_perms.write.granted_global = true; } ChildUnaryPermissionArg::NotGranted => {} ChildUnaryPermissionArg::GrantedList(granted_list) => { - worker_perms.write.granted_list = Permissions::new_write( + worker_perms.write.granted_list = parse_path_list( &Some(granted_list.iter().map(PathBuf::from).collect()), - false, - )? - .granted_list; + WriteDescriptor, + )?; if !worker_perms .write .granted_list @@ -2612,10 +2079,13 @@ pub fn create_child_permissions( } } } - worker_perms.write.denied_list = main_perms.write.denied_list.clone(); - if main_perms.write.global_state == PermissionState::Denied { - worker_perms.write.global_state = PermissionState::Denied; - } + worker_perms.write.flag_denied_global = main_perms.write.flag_denied_global; + worker_perms.write.flag_denied_list = + main_perms.write.flag_denied_list.clone(); + worker_perms.write.prompt_denied_global = + main_perms.write.prompt_denied_global; + worker_perms.write.prompt_denied_list = + main_perms.write.prompt_denied_list.clone(); worker_perms.write.prompt = main_perms.write.prompt; Ok(worker_perms) } @@ -2659,7 +2129,7 @@ mod tests { .is_ok()); assert!(perms .ffi - .check(Some(Path::new("/a/specific/dir/name"))) + .check(Path::new("/a/specific/dir/name"), None) .is_ok()); // Inside of /a/specific but outside of /a/specific/dir/name @@ -2668,7 +2138,7 @@ mod tests { .write .check(Path::new("/a/specific/dir"), None) .is_ok()); - assert!(perms.ffi.check(Some(Path::new("/a/specific/dir"))).is_ok()); + assert!(perms.ffi.check(Path::new("/a/specific/dir"), None).is_ok()); // Inside of /a/specific and /a/specific/dir/name assert!(perms @@ -2681,7 +2151,7 @@ mod tests { .is_ok()); assert!(perms .ffi - .check(Some(Path::new("/a/specific/dir/name/inner"))) + .check(Path::new("/a/specific/dir/name/inner"), None) .is_ok()); // Inside of /a/specific but outside of /a/specific/dir/name @@ -2695,18 +2165,18 @@ mod tests { .is_ok()); assert!(perms .ffi - .check(Some(Path::new("/a/specific/other/dir"))) + .check(Path::new("/a/specific/other/dir"), None) .is_ok()); // Exact match with /b/c assert!(perms.read.check(Path::new("/b/c"), None).is_ok()); assert!(perms.write.check(Path::new("/b/c"), None).is_ok()); - assert!(perms.ffi.check(Some(Path::new("/b/c"))).is_ok()); + assert!(perms.ffi.check(Path::new("/b/c"), None).is_ok()); // Sub path within /b/c assert!(perms.read.check(Path::new("/b/c/sub/path"), None).is_ok()); assert!(perms.write.check(Path::new("/b/c/sub/path"), None).is_ok()); - assert!(perms.ffi.check(Some(Path::new("/b/c/sub/path"))).is_ok()); + assert!(perms.ffi.check(Path::new("/b/c/sub/path"), None).is_ok()); // Sub path within /b/c, needs normalizing assert!(perms @@ -2719,18 +2189,18 @@ mod tests { .is_ok()); assert!(perms .ffi - .check(Some(Path::new("/b/c/sub/path/../path/."))) + .check(Path::new("/b/c/sub/path/../path/."), None) .is_ok()); // Inside of /b but outside of /b/c assert!(perms.read.check(Path::new("/b/e"), None).is_err()); assert!(perms.write.check(Path::new("/b/e"), None).is_err()); - assert!(perms.ffi.check(Some(Path::new("/b/e"))).is_err()); + assert!(perms.ffi.check(Path::new("/b/e"), None).is_err()); // Inside of /a but outside of /a/specific assert!(perms.read.check(Path::new("/a/b"), None).is_err()); assert!(perms.write.check(Path::new("/a/b"), None).is_err()); - assert!(perms.ffi.check(Some(Path::new("/a/b"))).is_err()); + assert!(perms.ffi.check(Path::new("/a/b"), None).is_err()); } #[test] @@ -2773,7 +2243,13 @@ mod tests { ]; for (host, port, is_ok) in domain_tests { - assert_eq!(is_ok, perms.net.check(&(host, Some(port)), None).is_ok()); + assert_eq!( + is_ok, + perms.net.check(&(host, Some(port)), None).is_ok(), + "{}:{}", + host, + port, + ); } } @@ -2905,7 +2381,7 @@ mod tests { for (url_str, is_ok) in url_tests { let u = url::Url::parse(url_str).unwrap(); - assert_eq!(is_ok, perms.net.check_url(&u, None).is_ok()); + assert_eq!(is_ok, perms.net.check_url(&u, None).is_ok(), "{}", u); } } @@ -2954,7 +2430,12 @@ mod tests { } for (specifier, expected) in fixtures { - assert_eq!(perms.check_specifier(&specifier).is_ok(), expected); + assert_eq!( + perms.check_specifier(&specifier).is_ok(), + expected, + "{}", + specifier, + ); } } @@ -2984,42 +2465,91 @@ mod tests { set_prompter(Box::new(TestPrompter)); let perms1 = Permissions::allow_all(); let perms2 = Permissions { - read: UnaryPermission { - global_state: PermissionState::Prompt, - ..Permissions::new_read(&Some(vec![PathBuf::from("/foo")]), false) - .unwrap() - }, - write: UnaryPermission { - global_state: PermissionState::Prompt, - ..Permissions::new_write(&Some(vec![PathBuf::from("/foo")]), false) - .unwrap() - }, - ffi: UnaryPermission { - global_state: PermissionState::Prompt, - ..Permissions::new_ffi(&Some(vec![PathBuf::from("/foo")]), false) - .unwrap() - }, - - net: UnaryPermission { - global_state: PermissionState::Prompt, - ..Permissions::new_net(&Some(svec!["127.0.0.1:8000"]), false).unwrap() - }, - env: UnaryPermission { - 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() - }, - hrtime: UnitPermission { - state: PermissionState::Prompt, - ..Permissions::new_hrtime(false) - }, + read: Permissions::new_read( + &Some(vec![PathBuf::from("/foo")]), + &None, + false, + ) + .unwrap(), + write: Permissions::new_write( + &Some(vec![PathBuf::from("/foo")]), + &None, + false, + ) + .unwrap(), + ffi: Permissions::new_ffi( + &Some(vec![PathBuf::from("/foo")]), + &None, + false, + ) + .unwrap(), + net: Permissions::new_net(&Some(svec!["127.0.0.1:8000"]), &None, false) + .unwrap(), + env: Permissions::new_env(&Some(svec!["HOME"]), &None, false).unwrap(), + sys: Permissions::new_sys(&Some(svec!["hostname"]), &None, false) + .unwrap(), + run: Permissions::new_run(&Some(svec!["deno"]), &None, false).unwrap(), + hrtime: Permissions::new_hrtime(false, false), + }; + let perms3 = Permissions { + read: Permissions::new_read( + &None, + &Some(vec![PathBuf::from("/foo")]), + false, + ) + .unwrap(), + write: Permissions::new_write( + &None, + &Some(vec![PathBuf::from("/foo")]), + false, + ) + .unwrap(), + ffi: Permissions::new_ffi( + &None, + &Some(vec![PathBuf::from("/foo")]), + false, + ) + .unwrap(), + net: Permissions::new_net(&None, &Some(svec!["127.0.0.1:8000"]), false) + .unwrap(), + env: Permissions::new_env(&None, &Some(svec!["HOME"]), false).unwrap(), + sys: Permissions::new_sys(&None, &Some(svec!["hostname"]), false) + .unwrap(), + run: Permissions::new_run(&None, &Some(svec!["deno"]), false).unwrap(), + hrtime: Permissions::new_hrtime(false, true), + }; + let perms4 = Permissions { + read: Permissions::new_read( + &Some(vec![]), + &Some(vec![PathBuf::from("/foo")]), + false, + ) + .unwrap(), + write: Permissions::new_write( + &Some(vec![]), + &Some(vec![PathBuf::from("/foo")]), + false, + ) + .unwrap(), + ffi: Permissions::new_ffi( + &Some(vec![]), + &Some(vec![PathBuf::from("/foo")]), + false, + ) + .unwrap(), + net: Permissions::new_net( + &Some(vec![]), + &Some(svec!["127.0.0.1:8000"]), + false, + ) + .unwrap(), + env: Permissions::new_env(&Some(vec![]), &Some(svec!["HOME"]), false) + .unwrap(), + sys: Permissions::new_sys(&Some(vec![]), &Some(svec!["hostname"]), false) + .unwrap(), + run: Permissions::new_run(&Some(vec![]), &Some(svec!["deno"]), false) + .unwrap(), + hrtime: Permissions::new_hrtime(true, true), }; #[rustfmt::skip] { @@ -3028,34 +2558,77 @@ mod tests { assert_eq!(perms2.read.query(None), PermissionState::Prompt); assert_eq!(perms2.read.query(Some(Path::new("/foo"))), PermissionState::Granted); assert_eq!(perms2.read.query(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms3.read.query(None), PermissionState::Prompt); + assert_eq!(perms3.read.query(Some(Path::new("/foo"))), PermissionState::Denied); + assert_eq!(perms3.read.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.read.query(None), PermissionState::GrantedPartial); + assert_eq!(perms4.read.query(Some(Path::new("/foo"))), PermissionState::Denied); + assert_eq!(perms4.read.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.read.query(Some(Path::new("/bar"))), PermissionState::Granted); assert_eq!(perms1.write.query(None), PermissionState::Granted); assert_eq!(perms1.write.query(Some(Path::new("/foo"))), PermissionState::Granted); assert_eq!(perms2.write.query(None), PermissionState::Prompt); assert_eq!(perms2.write.query(Some(Path::new("/foo"))), PermissionState::Granted); assert_eq!(perms2.write.query(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms3.write.query(None), PermissionState::Prompt); + assert_eq!(perms3.write.query(Some(Path::new("/foo"))), PermissionState::Denied); + assert_eq!(perms3.write.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.write.query(None), PermissionState::GrantedPartial); + assert_eq!(perms4.write.query(Some(Path::new("/foo"))), PermissionState::Denied); + assert_eq!(perms4.write.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.write.query(Some(Path::new("/bar"))), PermissionState::Granted); assert_eq!(perms1.ffi.query(None), PermissionState::Granted); assert_eq!(perms1.ffi.query(Some(Path::new("/foo"))), PermissionState::Granted); assert_eq!(perms2.ffi.query(None), PermissionState::Prompt); assert_eq!(perms2.ffi.query(Some(Path::new("/foo"))), PermissionState::Granted); assert_eq!(perms2.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms3.ffi.query(None), PermissionState::Prompt); + assert_eq!(perms3.ffi.query(Some(Path::new("/foo"))), PermissionState::Denied); + assert_eq!(perms3.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.ffi.query(None), PermissionState::GrantedPartial); + assert_eq!(perms4.ffi.query(Some(Path::new("/foo"))), PermissionState::Denied); + assert_eq!(perms4.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.ffi.query(Some(Path::new("/bar"))), PermissionState::Granted); assert_eq!(perms1.net.query::<&str>(None), PermissionState::Granted); assert_eq!(perms1.net.query(Some(&("127.0.0.1", None))), PermissionState::Granted); assert_eq!(perms2.net.query::<&str>(None), PermissionState::Prompt); assert_eq!(perms2.net.query(Some(&("127.0.0.1", Some(8000)))), PermissionState::Granted); + assert_eq!(perms3.net.query::<&str>(None), PermissionState::Prompt); + assert_eq!(perms3.net.query(Some(&("127.0.0.1", Some(8000)))), PermissionState::Denied); + assert_eq!(perms4.net.query::<&str>(None), PermissionState::GrantedPartial); + assert_eq!(perms4.net.query(Some(&("127.0.0.1", Some(8000)))), PermissionState::Denied); + assert_eq!(perms4.net.query(Some(&("192.168.0.1", Some(8000)))), PermissionState::Granted); assert_eq!(perms1.env.query(None), PermissionState::Granted); 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!(perms3.env.query(None), PermissionState::Prompt); + assert_eq!(perms3.env.query(Some("HOME")), PermissionState::Denied); + assert_eq!(perms4.env.query(None), PermissionState::GrantedPartial); + assert_eq!(perms4.env.query(Some("HOME")), PermissionState::Denied); + assert_eq!(perms4.env.query(Some("AWAY")), 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(None), PermissionState::Prompt); assert_eq!(perms2.sys.query(Some("hostname")), PermissionState::Granted); + assert_eq!(perms3.sys.query(None), PermissionState::Prompt); + assert_eq!(perms3.sys.query(Some("hostname")), PermissionState::Denied); + assert_eq!(perms4.sys.query(None), PermissionState::GrantedPartial); + assert_eq!(perms4.sys.query(Some("hostname")), PermissionState::Denied); + assert_eq!(perms4.sys.query(Some("uid")), 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); assert_eq!(perms2.run.query(Some("deno")), PermissionState::Granted); + assert_eq!(perms3.run.query(None), PermissionState::Prompt); + assert_eq!(perms3.run.query(Some("deno")), PermissionState::Denied); + assert_eq!(perms4.run.query(None), PermissionState::GrantedPartial); + assert_eq!(perms4.run.query(Some("deno")), PermissionState::Denied); + assert_eq!(perms4.run.query(Some("node")), PermissionState::Granted); assert_eq!(perms1.hrtime.query(), PermissionState::Granted); assert_eq!(perms2.hrtime.query(), PermissionState::Prompt); + assert_eq!(perms3.hrtime.query(), PermissionState::Denied); + assert_eq!(perms4.hrtime.query(), PermissionState::Denied); }; } @@ -3111,54 +2684,35 @@ mod tests { fn test_revoke() { set_prompter(Box::new(TestPrompter)); let mut perms = Permissions { - read: UnaryPermission { - global_state: PermissionState::Prompt, - ..Permissions::new_read( - &Some(vec![PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - false, - ) - .unwrap() - }, - write: UnaryPermission { - global_state: PermissionState::Prompt, - ..Permissions::new_write( - &Some(vec![PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - false, - ) - .unwrap() - }, - ffi: UnaryPermission { - global_state: PermissionState::Prompt, - ..Permissions::new_ffi( - &Some(vec![PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - false, - ) - .unwrap() - }, - net: UnaryPermission { - global_state: PermissionState::Prompt, - ..Permissions::new_net( - &Some(svec!["127.0.0.1", "127.0.0.1:8000"]), - false, - ) - .unwrap() - }, - env: UnaryPermission { - 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() - }, - hrtime: UnitPermission { - state: PermissionState::Denied, - ..Permissions::new_hrtime(false) - }, + read: Permissions::new_read( + &Some(vec![PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), + &None, + false, + ) + .unwrap(), + write: Permissions::new_write( + &Some(vec![PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), + &None, + false, + ) + .unwrap(), + ffi: Permissions::new_ffi( + &Some(vec![PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), + &None, + false, + ) + .unwrap(), + net: Permissions::new_net( + &Some(svec!["127.0.0.1", "127.0.0.1:8000"]), + &None, + false, + ) + .unwrap(), + env: Permissions::new_env(&Some(svec!["HOME"]), &None, false).unwrap(), + sys: Permissions::new_sys(&Some(svec!["hostname"]), &None, false) + .unwrap(), + run: Permissions::new_run(&Some(svec!["deno"]), &None, false).unwrap(), + hrtime: Permissions::new_hrtime(false, true), }; #[rustfmt::skip] { @@ -3185,14 +2739,14 @@ mod tests { fn test_check() { set_prompter(Box::new(TestPrompter)); let mut perms = Permissions { - read: Permissions::new_read(&None, true).unwrap(), - 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), + read: Permissions::new_read(&None, &None, true).unwrap(), + write: Permissions::new_write(&None, &None, true).unwrap(), + net: Permissions::new_net(&None, &None, true).unwrap(), + env: Permissions::new_env(&None, &None, true).unwrap(), + sys: Permissions::new_sys(&None, &None, true).unwrap(), + run: Permissions::new_run(&None, &None, true).unwrap(), + ffi: Permissions::new_ffi(&None, &None, true).unwrap(), + hrtime: Permissions::new_hrtime(false, false), }; let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); @@ -3210,10 +2764,10 @@ mod tests { assert!(perms.write.check(Path::new("/bar"), None).is_err()); prompt_value.set(true); - assert!(perms.ffi.check(Some(Path::new("/foo"))).is_ok()); + assert!(perms.ffi.check(Path::new("/foo"), None).is_ok()); prompt_value.set(false); - assert!(perms.ffi.check(Some(Path::new("/foo"))).is_ok()); - assert!(perms.ffi.check(Some(Path::new("/bar"))).is_err()); + assert!(perms.ffi.check(Path::new("/foo"), None).is_ok()); + assert!(perms.ffi.check(Path::new("/bar"), None).is_err()); prompt_value.set(true); assert!(perms.net.check(&("127.0.0.1", Some(8000)), None).is_ok()); @@ -3249,14 +2803,14 @@ mod tests { fn test_check_fail() { set_prompter(Box::new(TestPrompter)); let mut perms = Permissions { - read: Permissions::new_read(&None, true).unwrap(), - 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), + read: Permissions::new_read(&None, &None, true).unwrap(), + write: Permissions::new_write(&None, &None, true).unwrap(), + net: Permissions::new_net(&None, &None, true).unwrap(), + env: Permissions::new_env(&None, &None, true).unwrap(), + sys: Permissions::new_sys(&None, &None, true).unwrap(), + run: Permissions::new_run(&None, &None, true).unwrap(), + ffi: Permissions::new_ffi(&None, &None, true).unwrap(), + hrtime: Permissions::new_hrtime(false, false), }; let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); @@ -3278,12 +2832,12 @@ mod tests { assert!(perms.write.check(Path::new("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.ffi.check(Some(Path::new("/foo"))).is_err()); + assert!(perms.ffi.check(Path::new("/foo"), None).is_err()); prompt_value.set(true); - assert!(perms.ffi.check(Some(Path::new("/foo"))).is_err()); - assert!(perms.ffi.check(Some(Path::new("/bar"))).is_ok()); + assert!(perms.ffi.check(Path::new("/foo"), None).is_err()); + assert!(perms.ffi.check(Path::new("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.ffi.check(Some(Path::new("/bar"))).is_ok()); + assert!(perms.ffi.check(Path::new("/bar"), None).is_ok()); prompt_value.set(false); assert!(perms.net.check(&("127.0.0.1", Some(8000)), None).is_err()); @@ -3332,8 +2886,8 @@ mod tests { let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); let mut perms = Permissions::allow_all(); perms.env = UnaryPermission { - global_state: PermissionState::Prompt, - ..Permissions::new_env(&Some(svec!["HOME"]), false).unwrap() + granted_global: false, + ..Permissions::new_env(&Some(svec!["HOME"]), &None, false).unwrap() }; prompt_value.set(true); @@ -3504,9 +3058,10 @@ mod tests { fn test_create_child_permissions() { set_prompter(Box::new(TestPrompter)); let mut main_perms = Permissions { - env: Permissions::new_env(&Some(vec![]), false).unwrap(), - hrtime: Permissions::new_hrtime(true), - net: Permissions::new_net(&Some(svec!["foo", "bar"]), false).unwrap(), + env: Permissions::new_env(&Some(vec![]), &None, false).unwrap(), + hrtime: Permissions::new_hrtime(true, false), + net: Permissions::new_net(&Some(svec!["foo", "bar"]), &None, false) + .unwrap(), ..Default::default() }; assert_eq!( @@ -3522,8 +3077,8 @@ mod tests { ) .unwrap(), Permissions { - env: Permissions::new_env(&Some(vec![]), false).unwrap(), - net: Permissions::new_net(&Some(svec!["foo"]), false).unwrap(), + env: Permissions::new_env(&Some(vec![]), &None, false).unwrap(), + net: Permissions::new_net(&Some(svec!["foo"]), &None, false).unwrap(), ..Default::default() } ); @@ -3591,18 +3146,36 @@ mod tests { ChildPermissionsArg::none(), ) .unwrap(); - assert_eq!(worker_perms.write.denied_list, main_perms.write.denied_list); + assert_eq!( + worker_perms.write.flag_denied_list, + main_perms.write.flag_denied_list + ); } #[test] fn test_handle_empty_value() { set_prompter(Box::new(TestPrompter)); - 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()); - assert!(Permissions::new_write(&Some(vec![PathBuf::new()]), false).is_err()); + assert!( + Permissions::new_read(&Some(vec![PathBuf::new()]), &None, false).is_err() + ); + assert!( + Permissions::new_env(&Some(vec![String::new()]), &None, false).is_err() + ); + assert!( + Permissions::new_sys(&Some(vec![String::new()]), &None, false).is_err() + ); + assert!( + Permissions::new_run(&Some(vec![String::new()]), &None, false).is_err() + ); + assert!( + Permissions::new_ffi(&Some(vec![PathBuf::new()]), &None, false).is_err() + ); + assert!( + Permissions::new_net(&Some(svec![String::new()]), &None, false).is_err() + ); + assert!( + Permissions::new_write(&Some(vec![PathBuf::new()]), &None, false) + .is_err() + ); } }