diff options
author | Casper Beyer <caspervonb@pm.me> | 2021-04-26 05:38:59 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-25 23:38:59 +0200 |
commit | f3751e498faabd524494a4b70c49b1f53fe5ebdd (patch) | |
tree | 9f3cf5b15fc10d0ef12f78c984fbbb59554343ef /cli | |
parent | 7063e449f11ab2cff492ba90314da7a0bcd994a6 (diff) |
feat(cli): add test permissions to Deno.test (#10188)
This commits adds adds "permissions" option to the test definitions
which allows tests to run with different permission sets than
the process's permission.
The change will only be in effect within the test function, once the
test has completed the original process permission set is restored.
Test permissions cannot exceed the process's permission.
You can only narrow or drop permissions, failure to acquire a
permission results in an error being thrown and the test case will fail.
Diffstat (limited to 'cli')
-rw-r--r-- | cli/Cargo.toml | 2 | ||||
-rw-r--r-- | cli/dts/lib.deno.unstable.d.ts | 134 | ||||
-rw-r--r-- | cli/main.rs | 25 | ||||
-rw-r--r-- | cli/ops/mod.rs | 1 | ||||
-rw-r--r-- | cli/ops/test_runner.rs | 66 | ||||
-rw-r--r-- | cli/tests/integration_tests.rs | 12 | ||||
-rw-r--r-- | cli/tests/test/allow_all.out | 18 | ||||
-rw-r--r-- | cli/tests/test/allow_all.ts | 35 | ||||
-rw-r--r-- | cli/tests/test/allow_none.out | 51 | ||||
-rw-r--r-- | cli/tests/test/allow_none.ts | 23 |
10 files changed, 359 insertions, 8 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 909ec1537..90fa80593 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 d955a825f..76d90b10d 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 `"<host>[:<port>]`. + * + * 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<string | URL>`, the `read` permission will be requested with the + * specified file paths. + * + * Defaults to "inherit". + */ + read?: "inherit" | boolean | Array<string | URL>; + + /** 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<string | URL>`, the `write` permission will be requested with the + * specified file paths. + * + * Defaults to "inherit". + */ + write?: "inherit" | boolean | Array<string | URL>; + }; + } } declare function fetch( diff --git a/cli/main.rs b/cli/main.rs index c42604146..38e67f46a 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -160,6 +160,7 @@ pub fn create_main_worker( program_state: &Arc<ProgramState>, 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 cce0625c6..386ad16fa 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 000000000..380ec7fb0 --- /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<ZeroCopyBuf>, +) -> Result<Uuid, AnyError> { + deno_runtime::ops::check_unstable(state, "Deno.test.permissions"); + + let token = Uuid::new_v4(); + let parent_permissions = state.borrow::<Permissions>().clone(); + let worker_permissions = { + let permissions: PermissionsArg = serde_json::from_value(args)?; + create_worker_permissions(parent_permissions.clone(), permissions)? + }; + + state.put::<PermissionsHolder>(PermissionsHolder(token, parent_permissions)); + + // NOTE: This call overrides current permission set for the worker + state.put::<Permissions>(worker_permissions); + + Ok(token) +} + +pub fn op_restore_test_permissions( + state: &mut OpState, + token: Uuid, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<(), AnyError> { + deno_runtime::ops::check_unstable(state, "Deno.test.permissions"); + + if let Some(permissions_holder) = state.try_take::<PermissionsHolder>() { + if token != permissions_holder.0 { + panic!("restore test permissions token does not match the stored token"); + } + + let permissions = permissions_holder.1; + state.put::<Permissions>(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 28d2dc7c6..770e87244 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 000000000..3edb88d0f --- /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 000000000..e4e12144e --- /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 000000000..6565a0800 --- /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 000000000..c0a930eb1 --- /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(); + }, + }); +} |