diff --git a/Cargo.lock b/Cargo.lock index a27cce71897741..f505f318c1ca1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3897,6 +3897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ "getrandom 0.2.2", + "serde", ] [[package]] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 909ec1537001fc..90fa8059348ae6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -78,7 +78,7 @@ termcolor = "1.1.2" text-size = "1.1.0" tokio = { version = "1.4.0", features = ["full"] } tokio-rustls = "0.22.0" -uuid = { version = "0.8.2", features = ["v4"] } +uuid = { version = "0.8.2", features = ["v4", "serde"] } walkdir = "2.3.2" [target.'cfg(windows)'.dependencies] diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 908c433bfebbc3..05e4fe37285115 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -1181,6 +1181,140 @@ declare namespace Deno { * then the underlying HttpConn resource is closed automatically. */ export function serveHttp(conn: Conn): HttpConn; + + /** **UNSTABLE**: New option, yet to be vetted. */ + export interface TestDefinition { + /** Specifies the permissions that should be used to run the test. + * Set this to "inherit" to keep the calling thread's permissions. + * Set this to "none" to revoke all permissions. + * + * Defaults to "inherit". + */ + permissions?: "inherit" | "none" | { + /** Specifies if the `net` permission should be requested or revoked. + * If set to `"inherit"`, the current `env` permission will be inherited. + * If set to `true`, the global `net` permission will be requested. + * If set to `false`, the global `net` permission will be revoked. + * + * Defaults to "inherit". + */ + env?: "inherit" | boolean; + + /** Specifies if the `hrtime` permission should be requested or revoked. + * If set to `"inherit"`, the current `hrtime` permission will be inherited. + * If set to `true`, the global `hrtime` permission will be requested. + * If set to `false`, the global `hrtime` permission will be revoked. + * + * Defaults to "inherit". + */ + hrtime?: "inherit" | boolean; + + /** Specifies if the `net` permission should be requested or revoked. + * if set to `"inherit"`, the current `net` permission will be inherited. + * if set to `true`, the global `net` permission will be requested. + * if set to `false`, the global `net` permission will be revoked. + * if set to `string[]`, the `net` permission will be requested with the + * specified host strings with the format `"[:]`. + * + * Defaults to "inherit". + * + * Examples: + * + * ``` + * Deno.test({ + * name: "inherit", + * permissions: { + * net: "inherit", + * }, + * async fn() { + * const status = await Deno.permissions.query({ name: "net" }) + * assertEquals(status.state, "granted"); + * }, + * }; + * ``` + * + * ``` + * Deno.test({ + * name: "true", + * permissions: { + * net: true, + * }, + * async fn() { + * const status = await Deno.permissions.query({ name: "net" }); + * assertEquals(status.state, "granted"); + * }, + * }; + * ``` + * + * ``` + * Deno.test({ + * name: "false", + * permissions: { + * net: false, + * }, + * async fn() { + * const status = await Deno.permissions.query({ name: "net" }); + * assertEquals(status.state, "denied"); + * }, + * }; + * ``` + * + * ``` + * Deno.test({ + * name: "localhost:8080", + * permissions: { + * net: ["localhost:8080"], + * }, + * async fn() { + * const status = await Deno.permissions.query({ name: "net", host: "localhost:8080" }); + * assertEquals(status.state, "granted"); + * }, + * }; + * ``` + */ + net?: "inherit" | boolean | string[]; + + /** Specifies if the `plugin` permission should be requested or revoked. + * If set to `"inherit"`, the current `plugin` permission will be inherited. + * If set to `true`, the global `plugin` permission will be requested. + * If set to `false`, the global `plugin` permission will be revoked. + * + * Defaults to "inherit". + */ + plugin?: "inherit" | boolean; + + /** Specifies if the `read` permission should be requested or revoked. + * If set to `"inherit"`, the current `read` permission will be inherited. + * If set to `true`, the global `read` permission will be requested. + * If set to `false`, the global `read` permission will be revoked. + * If set to `Array`, the `read` permission will be requested with the + * specified file paths. + * + * Defaults to "inherit". + */ + read?: "inherit" | boolean | Array; + + /** Specifies if the `run` permission should be requested or revoked. + * If set to `"inherit"`, the current `run` permission will be inherited. + * If set to `true`, the global `run` permission will be requested. + * If set to `false`, the global `run` permission will be revoked. + * + * Defaults to "inherit". + */ + run?: "inherit" | boolean; + + /** Specifies if the `write` permission should be requested or revoked. + * If set to `"inherit"`, the current `write` permission will be inherited. + * If set to `true`, the global `write` permission will be requested. + * If set to `false`, the global `write` permission will be revoked. + * If set to `Array`, the `write` permission will be requested with the + * specified file paths. + * + * Defaults to "inherit". + */ + write?: "inherit" | boolean | Array; + }; + } } declare function fetch( diff --git a/cli/main.rs b/cli/main.rs index c42604146f30b2..38e67f46a4e5df 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -160,6 +160,7 @@ pub fn create_main_worker( program_state: &Arc, main_module: ModuleSpecifier, permissions: Permissions, + enable_testing: bool, ) -> MainWorker { let module_loader = CliModuleLoader::new(program_state.clone()); @@ -219,6 +220,11 @@ pub fn create_main_worker( // above ops::errors::init(js_runtime); ops::runtime_compiler::init(js_runtime); + + if enable_testing { + ops::test_runner::init(js_runtime); + } + js_runtime.sync_ops_cache(); } worker.bootstrap(&options); @@ -427,7 +433,7 @@ async fn install_command( let program_state = ProgramState::build(preload_flags).await?; let main_module = resolve_url_or_path(&module_url)?; let mut worker = - create_main_worker(&program_state, main_module.clone(), permissions); + create_main_worker(&program_state, main_module.clone(), permissions, false); // First, fetch and compile the module; this step ensures that the module exists. worker.preload_module(&main_module).await?; tools::installer::install(flags, &module_url, args, name, root, force) @@ -494,7 +500,7 @@ async fn eval_command( let permissions = Permissions::from_options(&flags.clone().into()); let program_state = ProgramState::build(flags).await?; let mut worker = - create_main_worker(&program_state, main_module.clone(), permissions); + create_main_worker(&program_state, main_module.clone(), permissions, false); // Create a dummy source file. let source_code = if print { format!("console.log({})", code) @@ -728,7 +734,7 @@ async fn run_repl(flags: Flags) -> Result<(), AnyError> { let permissions = Permissions::from_options(&flags.clone().into()); let program_state = ProgramState::build(flags).await?; let mut worker = - create_main_worker(&program_state, main_module.clone(), permissions); + create_main_worker(&program_state, main_module.clone(), permissions, false); worker.run_event_loop().await?; tools::repl::run(&program_state, worker).await @@ -742,6 +748,7 @@ async fn run_from_stdin(flags: Flags) -> Result<(), AnyError> { &program_state.clone(), main_module.clone(), permissions, + false, ); let mut source = Vec::new(); @@ -819,8 +826,12 @@ async fn run_with_watch(flags: Flags, script: String) -> Result<(), AnyError> { async move { let main_module = main_module.clone(); let program_state = ProgramState::build(flags).await?; - let mut worker = - create_main_worker(&program_state, main_module.clone(), permissions); + let mut worker = create_main_worker( + &program_state, + main_module.clone(), + permissions, + false, + ); debug!("main_module {}", main_module); worker.execute_module(&main_module).await?; worker.execute("window.dispatchEvent(new Event('load'))")?; @@ -853,7 +864,7 @@ async fn run_command(flags: Flags, script: String) -> Result<(), AnyError> { let program_state = ProgramState::build(flags.clone()).await?; let permissions = Permissions::from_options(&flags.clone().into()); let mut worker = - create_main_worker(&program_state, main_module.clone(), permissions); + create_main_worker(&program_state, main_module.clone(), permissions, false); let mut maybe_coverage_collector = if let Some(ref coverage_dir) = program_state.coverage_dir { @@ -970,7 +981,7 @@ async fn test_command( } let mut worker = - create_main_worker(&program_state, main_module.clone(), permissions); + create_main_worker(&program_state, main_module.clone(), permissions, true); if let Some(ref coverage_dir) = flags.coverage_dir { env::set_var("DENO_UNSTABLE_COVERAGE_DIR", coverage_dir); diff --git a/cli/ops/mod.rs b/cli/ops/mod.rs index cce0625c68cc5c..386ad16fa3d64c 100644 --- a/cli/ops/mod.rs +++ b/cli/ops/mod.rs @@ -2,5 +2,6 @@ pub mod errors; pub mod runtime_compiler; +pub mod test_runner; pub use deno_runtime::ops::{reg_async, reg_sync}; diff --git a/cli/ops/test_runner.rs b/cli/ops/test_runner.rs new file mode 100644 index 00000000000000..380ec7fb000b71 --- /dev/null +++ b/cli/ops/test_runner.rs @@ -0,0 +1,66 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::serde_json::Value; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use deno_runtime::ops::worker_host::create_worker_permissions; +use deno_runtime::ops::worker_host::PermissionsArg; +use deno_runtime::permissions::Permissions; +use uuid::Uuid; + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_sync(rt, "op_pledge_test_permissions", op_pledge_test_permissions); + super::reg_sync( + rt, + "op_restore_test_permissions", + op_restore_test_permissions, + ); +} + +#[derive(Clone)] +struct PermissionsHolder(Uuid, Permissions); + +pub fn op_pledge_test_permissions( + state: &mut OpState, + args: Value, + _zero_copy: Option, +) -> Result { + deno_runtime::ops::check_unstable(state, "Deno.test.permissions"); + + let token = Uuid::new_v4(); + let parent_permissions = state.borrow::().clone(); + let worker_permissions = { + let permissions: PermissionsArg = serde_json::from_value(args)?; + create_worker_permissions(parent_permissions.clone(), permissions)? + }; + + state.put::(PermissionsHolder(token, parent_permissions)); + + // NOTE: This call overrides current permission set for the worker + state.put::(worker_permissions); + + Ok(token) +} + +pub fn op_restore_test_permissions( + state: &mut OpState, + token: Uuid, + _zero_copy: Option, +) -> Result<(), AnyError> { + deno_runtime::ops::check_unstable(state, "Deno.test.permissions"); + + if let Some(permissions_holder) = state.try_take::() { + if token != permissions_holder.0 { + panic!("restore test permissions token does not match the stored token"); + } + + let permissions = permissions_holder.1; + state.put::(permissions); + Ok(()) + } else { + Err(generic_error("no permissions to restore")) + } +} diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 28d2dc7c60f7b4..770e87244665ce 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -2395,6 +2395,18 @@ mod integration { output: "test/deno_test.out", }); + itest!(allow_all { + args: "test --unstable --allow-all test/allow_all.ts", + exit_code: 0, + output: "test/allow_all.out", + }); + + itest!(allow_none { + args: "test --unstable test/allow_none.ts", + exit_code: 1, + output: "test/allow_none.out", + }); + itest!(fail_fast { args: "test --fail-fast test/test_runner_test.ts", exit_code: 1, diff --git a/cli/tests/test/allow_all.out b/cli/tests/test/allow_all.out new file mode 100644 index 00000000000000..3edb88d0f5a6c3 --- /dev/null +++ b/cli/tests/test/allow_all.out @@ -0,0 +1,18 @@ +[WILDCARD] +running 14 tests +test read false ... ok [WILDCARD] +test read true ... ok [WILDCARD] +test write false ... ok [WILDCARD] +test write true ... ok [WILDCARD] +test net false ... ok [WILDCARD] +test net true ... ok [WILDCARD] +test env false ... ok [WILDCARD] +test env true ... ok [WILDCARD] +test run false ... ok [WILDCARD] +test run true ... ok [WILDCARD] +test plugin false ... ok [WILDCARD] +test plugin true ... ok [WILDCARD] +test hrtime false ... ok [WILDCARD] +test hrtime true ... ok [WILDCARD] + +test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out [WILDCARD] diff --git a/cli/tests/test/allow_all.ts b/cli/tests/test/allow_all.ts new file mode 100644 index 00000000000000..e4e12144ee7508 --- /dev/null +++ b/cli/tests/test/allow_all.ts @@ -0,0 +1,35 @@ +import { assertEquals } from "../../../test_util/std/testing/asserts.ts"; + +const permissions: Deno.PermissionName[] = [ + "read", + "write", + "net", + "env", + "run", + "plugin", + "hrtime", +]; + +for (const name of permissions) { + Deno.test({ + name: `${name} false`, + permissions: { + [name]: false, + }, + async fn() { + const status = await Deno.permissions.query({ name }); + assertEquals(status.state, "denied"); + }, + }); + + Deno.test({ + name: `${name} true`, + permissions: { + [name]: true, + }, + async fn() { + const status = await Deno.permissions.query({ name }); + assertEquals(status.state, "granted"); + }, + }); +} diff --git a/cli/tests/test/allow_none.out b/cli/tests/test/allow_none.out new file mode 100644 index 00000000000000..6565a0800d8764 --- /dev/null +++ b/cli/tests/test/allow_none.out @@ -0,0 +1,51 @@ +[WILDCARD] +running 7 tests +test read ... FAILED [WILDCARD] +test write ... FAILED [WILDCARD] +test net ... FAILED [WILDCARD] +test env ... FAILED [WILDCARD] +test run ... FAILED [WILDCARD] +test plugin ... FAILED [WILDCARD] +test hrtime ... FAILED [WILDCARD] + +failures: + +read +PermissionDenied: Can't escalate parent thread permissions +[WILDCARD] + +write +PermissionDenied: Can't escalate parent thread permissions +[WILDCARD] + +net +PermissionDenied: Can't escalate parent thread permissions +[WILDCARD] + +env +PermissionDenied: Can't escalate parent thread permissions +[WILDCARD] + +run +PermissionDenied: Can't escalate parent thread permissions +[WILDCARD] + +plugin +PermissionDenied: Can't escalate parent thread permissions +[WILDCARD] + +hrtime +PermissionDenied: Can't escalate parent thread permissions +[WILDCARD] + +failures: + + read + write + net + env + run + plugin + hrtime + +test result: FAILED. 0 passed; 7 failed; 0 ignored; 0 measured; 0 filtered out [WILDCARD] diff --git a/cli/tests/test/allow_none.ts b/cli/tests/test/allow_none.ts new file mode 100644 index 00000000000000..c0a930eb1210d4 --- /dev/null +++ b/cli/tests/test/allow_none.ts @@ -0,0 +1,23 @@ +import { unreachable } from "../../../test_util/std/testing/asserts.ts"; + +const permissions: Deno.PermissionName[] = [ + "read", + "write", + "net", + "env", + "run", + "plugin", + "hrtime", +]; + +for (const name of permissions) { + Deno.test({ + name, + permissions: { + [name]: true, + }, + fn() { + unreachable(); + }, + }); +} diff --git a/runtime/js/11_workers.js b/runtime/js/11_workers.js index 2c602ab60653ac..508dd46d4811ed 100644 --- a/runtime/js/11_workers.js +++ b/runtime/js/11_workers.js @@ -333,6 +333,7 @@ defineEventHandler(Worker.prototype, "messageerror"); window.__bootstrap.worker = { + parsePermissions, Worker, }; })(this); diff --git a/runtime/js/40_testing.js b/runtime/js/40_testing.js index 7666fa050a8169..4a97f64372657f 100644 --- a/runtime/js/40_testing.js +++ b/runtime/js/40_testing.js @@ -4,6 +4,7 @@ ((window) => { const core = window.Deno.core; const colors = window.__bootstrap.colors; + const { parsePermissions } = window.__bootstrap.worker; const { setExitHandler, exit } = window.__bootstrap.os; const { Console, inspectArgs } = window.__bootstrap.console; const { stdout } = window.__bootstrap.files; @@ -121,6 +122,7 @@ finishing test case.`; sanitizeOps: true, sanitizeResources: true, sanitizeExit: true, + permissions: null, }; if (typeof t === "string") { @@ -226,6 +228,17 @@ finishing test case.`; } } + function pledgeTestPermissions(permissions) { + return core.opSync( + "op_pledge_test_permissions", + parsePermissions(permissions), + ); + } + + function restoreTestPermissions(token) { + core.opSync("op_restore_test_permissions", token); + } + // TODO(bartlomieju): already implements AsyncGenerator, but add as "implements to class" // TODO(bartlomieju): implements PromiseLike class TestRunner { @@ -257,6 +270,7 @@ finishing test case.`; const results = []; const suiteStart = +new Date(); + for (const test of this.testsToRun) { const endMessage = { name: test.name, @@ -268,15 +282,30 @@ finishing test case.`; this.stats.ignored++; } else { const start = +new Date(); + + let token; try { + if (test.permissions) { + token = pledgeTestPermissions(test.permissions); + } + await test.fn(); + endMessage.status = "passed"; this.stats.passed++; } catch (err) { endMessage.status = "failed"; endMessage.error = err; this.stats.failed++; + } finally { + // Permissions must always be restored for a clean environment, + // otherwise the process can end up dropping permissions + // until there are none left. + if (token) { + restoreTestPermissions(token); + } } + endMessage.duration = +new Date() - start; } results.push(endMessage); diff --git a/runtime/ops/worker_host.rs b/runtime/ops/worker_host.rs index 905ae1334b2e2e..ff861c01d38837 100644 --- a/runtime/ops/worker_host.rs +++ b/runtime/ops/worker_host.rs @@ -228,7 +228,7 @@ fn merge_run_permission( Ok(main) } -fn create_worker_permissions( +pub fn create_worker_permissions( main_perms: Permissions, worker_perms: PermissionsArg, ) -> Result { @@ -244,7 +244,7 @@ fn create_worker_permissions( } #[derive(Debug, Deserialize)] -struct PermissionsArg { +pub struct PermissionsArg { #[serde(default, deserialize_with = "as_unary_env_permission")] env: Option>, #[serde(default, deserialize_with = "as_permission_state")]