1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-23 15:49:44 -05:00

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.
This commit is contained in:
Casper Beyer 2021-04-26 05:38:59 +08:00 committed by GitHub
parent 7063e449f1
commit f3751e498f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 392 additions and 10 deletions

1
Cargo.lock generated
View file

@ -3897,6 +3897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom 0.2.2",
"serde",
]
[[package]]

View file

@ -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]

View file

@ -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(

View file

@ -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);

View file

@ -2,5 +2,6 @@
pub mod errors;
pub mod runtime_compiler;
pub mod test_runner;
pub use deno_runtime::ops::{reg_async, reg_sync};

66
cli/ops/test_runner.rs Normal file
View file

@ -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"))
}
}

View file

@ -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,

View file

@ -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]

View file

@ -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");
},
});
}

View file

@ -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]

View file

@ -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();
},
});
}

View file

@ -333,6 +333,7 @@
defineEventHandler(Worker.prototype, "messageerror");
window.__bootstrap.worker = {
parsePermissions,
Worker,
};
})(this);

View file

@ -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<RunTestsMessage>, but add as "implements to class"
// TODO(bartlomieju): implements PromiseLike<RunTestsEndResult>
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);

View file

@ -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<Permissions, AnyError> {
@ -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<UnaryPermission<EnvDescriptor>>,
#[serde(default, deserialize_with = "as_permission_state")]