diff --git a/cli/dts/lib.deno.ns.d.ts b/cli/dts/lib.deno.ns.d.ts index eb91d6fa46..3fff1c61ed 100644 --- a/cli/dts/lib.deno.ns.d.ts +++ b/cli/dts/lib.deno.ns.d.ts @@ -2178,6 +2178,7 @@ declare namespace Deno { export interface FfiPermissionDescriptor { name: "ffi"; + path?: string | URL; } export interface HrtimePermissionDescriptor { diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 3bea165e51..fd33e1a74f 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -910,10 +910,12 @@ declare namespace Deno { * If set to `"inherit"`, the current `ffi` permission will be inherited. * If set to `true`, the global `ffi` permission will be requested. * If set to `false`, the global `ffi` permission will be revoked. + * If set to `Array`, the `ffi` permission will be requested with the + * specified file paths. * * Defaults to "inherit". */ - ffi?: "inherit" | boolean; + ffi?: "inherit" | boolean | Array; /** Specifies if the `read` permission should be requested or revoked. * If set to `"inherit"`, the current `read` permission will be inherited. @@ -1237,7 +1239,7 @@ declare interface WorkerOptions { * For example: `["https://deno.land", "localhost:8080"]`. */ net?: "inherit" | boolean | string[]; - ffi?: "inherit" | boolean; + ffi?: "inherit" | boolean | Array; read?: "inherit" | boolean | Array; run?: "inherit" | boolean | Array; write?: "inherit" | boolean | Array; diff --git a/cli/flags.rs b/cli/flags.rs index df17c08dab..67c76244da 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -198,7 +198,7 @@ pub struct Flags { pub allow_env: Option>, pub allow_hrtime: bool, pub allow_net: Option>, - pub allow_ffi: Option>, + pub allow_ffi: Option>, pub allow_read: Option>, pub allow_run: Option>, pub allow_write: Option>, @@ -324,7 +324,7 @@ impl Flags { args.push("--allow-ffi".to_string()); } Some(ffi_allowlist) => { - let s = format!("--allow-ffi={}", ffi_allowlist.join(",")); + let s = format!("--allow-ffi={}", join_paths(ffi_allowlist, ",")); args.push(s); } _ => {} @@ -1685,10 +1685,10 @@ fn config_arg<'a, 'b>() -> Arg<'a, 'b> { .long_help( "Load configuration file. Before 1.14 Deno only supported loading tsconfig.json that allowed -to customise TypeScript compiler settings. +to customise TypeScript compiler settings. -Starting with 1.14 configuration file can be used to configure different -subcommands like `deno lint` or `deno fmt`. +Starting with 1.14 configuration file can be used to configure different +subcommands like `deno lint` or `deno fmt`. It's recommended to use `deno.json` or `deno.jsonc` as a filename.", ) @@ -2202,7 +2202,7 @@ fn permission_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) { } if let Some(ffi_wl) = matches.values_of("allow-ffi") { - let ffi_allowlist: Vec = ffi_wl.map(ToString::to_string).collect(); + let ffi_allowlist: Vec = ffi_wl.map(PathBuf::from).collect(); flags.allow_ffi = Some(ffi_allowlist); debug!("ffi allowlist: {:#?}", &flags.allow_ffi); } diff --git a/cli/ops/testing.rs b/cli/ops/testing.rs index 99cfc670e6..31b60b4800 100644 --- a/cli/ops/testing.rs +++ b/cli/ops/testing.rs @@ -4,8 +4,8 @@ use deno_core::error::AnyError; use deno_core::JsRuntime; use deno_core::ModuleSpecifier; use deno_core::OpState; -use deno_runtime::ops::worker_host::create_worker_permissions; -use deno_runtime::ops::worker_host::PermissionsArg; +use deno_runtime::permissions::create_child_permissions; +use deno_runtime::permissions::ChildPermissionsArg; use deno_runtime::permissions::Permissions; use std::sync::mpsc::Sender; use uuid::Uuid; @@ -26,15 +26,15 @@ struct PermissionsHolder(Uuid, Permissions); pub fn op_pledge_test_permissions( state: &mut OpState, - args: PermissionsArg, + args: ChildPermissionsArg, _: (), ) -> Result { deno_runtime::ops::check_unstable(state, "Deno.test.permissions"); let token = Uuid::new_v4(); - let parent_permissions = state.borrow::().clone(); - let worker_permissions = - create_worker_permissions(parent_permissions.clone(), args)?; + let parent_permissions = state.borrow_mut::(); + let worker_permissions = create_child_permissions(parent_permissions, args)?; + let parent_permissions = parent_permissions.clone(); state.put::(PermissionsHolder(token, parent_permissions)); diff --git a/cli/tests/integration/worker_tests.rs b/cli/tests/integration/worker_tests.rs index bf10578216..c17b63af97 100644 --- a/cli/tests/integration/worker_tests.rs +++ b/cli/tests/integration/worker_tests.rs @@ -3,7 +3,7 @@ use crate::itest; itest!(workers { - args: "test --reload --location http://127.0.0.1:4545/ --allow-net --allow-read --unstable workers/test.ts", + args: "test --reload --location http://127.0.0.1:4545/ -A --unstable workers/test.ts", output: "workers/test.ts.out", http_server: true, }); diff --git a/cli/tests/testdata/test/allow_all.ts b/cli/tests/testdata/test/allow_all.ts index abe55a8d53..e70ac46b02 100644 --- a/cli/tests/testdata/test/allow_all.ts +++ b/cli/tests/testdata/test/allow_all.ts @@ -18,7 +18,7 @@ for (const name of permissions) { }, async fn() { const status = await Deno.permissions.query({ name }); - assertEquals(status.state, "denied"); + assertEquals(status.state, "prompt"); }, }); diff --git a/cli/tests/testdata/workers/no_permissions_worker.js b/cli/tests/testdata/workers/no_permissions_worker.js index db0d911ac9..f49f690abe 100644 --- a/cli/tests/testdata/workers/no_permissions_worker.js +++ b/cli/tests/testdata/workers/no_permissions_worker.js @@ -6,12 +6,12 @@ self.onmessage = async () => { const run = await Deno.permissions.query({ name: "run" }); const write = await Deno.permissions.query({ name: "write" }); self.postMessage( - hrtime.state === "denied" && - net.state === "denied" && - ffi.state === "denied" && - read.state === "denied" && - run.state === "denied" && - write.state === "denied", + hrtime.state === "prompt" && + net.state === "prompt" && + ffi.state === "prompt" && + read.state === "prompt" && + run.state === "prompt" && + write.state === "prompt", ); self.close(); }; diff --git a/cli/tests/testdata/workers/parent_read_check_granular_worker.js b/cli/tests/testdata/workers/parent_read_check_granular_worker.js deleted file mode 100644 index 1391190cdd..0000000000 --- a/cli/tests/testdata/workers/parent_read_check_granular_worker.js +++ /dev/null @@ -1,41 +0,0 @@ -const worker = new Worker( - new URL("./read_check_granular_worker.js", import.meta.url).href, - { - type: "module", - deno: { - namespace: true, - permissions: { - read: [], - }, - }, - }, -); - -let received = 0; -const messages = []; - -worker.onmessage = ({ data: childResponse }) => { - received++; - postMessage({ - childHasPermission: childResponse.hasPermission, - index: childResponse.index, - parentHasPermission: messages[childResponse.index], - }); - if (received === messages.length) { - worker.terminate(); - } -}; - -onmessage = async ({ data }) => { - const { state } = await Deno.permissions.query({ - name: "read", - path: data.path, - }); - - messages[data.index] = state === "granted"; - - worker.postMessage({ - index: data.index, - route: data.route, - }); -}; diff --git a/cli/tests/testdata/workers/parent_read_check_worker.js b/cli/tests/testdata/workers/parent_read_check_worker.js index ec92cca3fb..87ea6bdedb 100644 --- a/cli/tests/testdata/workers/parent_read_check_worker.js +++ b/cli/tests/testdata/workers/parent_read_check_worker.js @@ -1,27 +1,18 @@ -onmessage = async () => { - const { state } = await Deno.permissions.query({ - name: "read", - }); - - const worker = new Worker( - new URL("./read_check_worker.js", import.meta.url).href, - { - type: "module", - deno: { - namespace: true, - permissions: { - read: false, - }, - }, +const worker = new Worker( + new URL("./read_check_granular_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: "none", }, - ); + }, +); - worker.onmessage = ({ data: childHasPermission }) => { - postMessage({ - parentHasPermission: state === "granted", - childHasPermission, - }); - close(); - }; - worker.postMessage(null); +onmessage = ({ data }) => { + worker.postMessage(data); +}; + +worker.onmessage = ({ data }) => { + postMessage(data); }; diff --git a/cli/tests/testdata/workers/read_check_granular_worker.js b/cli/tests/testdata/workers/read_check_granular_worker.js index 25f2058b3e..d40fac876c 100644 --- a/cli/tests/testdata/workers/read_check_granular_worker.js +++ b/cli/tests/testdata/workers/read_check_granular_worker.js @@ -1,11 +1,29 @@ -onmessage = async ({ data }) => { - const { state } = await Deno.permissions.query({ - name: "read", - path: data.path, - }); - - postMessage({ - hasPermission: state === "granted", - index: data.index, - }); -}; +// deno-fmt-ignore-file +postMessage({ + envGlobal: (await Deno.permissions.query({ name: "env" })).state, + envFoo: (await Deno.permissions.query({ name: "env", variable: "foo" })).state, + envAbsent: (await Deno.permissions.query({ name: "env", variable: "absent" })).state, + hrtime: (await Deno.permissions.query({ name: "hrtime" })).state, + netGlobal: (await Deno.permissions.query({ name: "net" })).state, + netFoo: (await Deno.permissions.query({ name: "net", host: "foo" })).state, + netFoo8000: (await Deno.permissions.query({ name: "net", host: "foo:8000" })).state, + netBar: (await Deno.permissions.query({ name: "net", host: "bar" })).state, + netBar8000: (await Deno.permissions.query({ name: "net", host: "bar:8000" })).state, + ffiGlobal: (await Deno.permissions.query({ name: "ffi" })).state, + ffiFoo: (await Deno.permissions.query({ name: "ffi", path: new URL("foo", import.meta.url) })).state, + ffiBar: (await Deno.permissions.query({ name: "ffi", path: "bar" })).state, + ffiAbsent: (await Deno.permissions.query({ name: "ffi", path: "absent" })).state, + readGlobal: (await Deno.permissions.query({ name: "read" })).state, + readFoo: (await Deno.permissions.query({ name: "read", path: new URL("foo", import.meta.url) })).state, + readBar: (await Deno.permissions.query({ name: "read", path: "bar" })).state, + readAbsent: (await Deno.permissions.query({ name: "read", path: "absent" })).state, + runGlobal: (await Deno.permissions.query({ name: "run" })).state, + runFoo: (await Deno.permissions.query({ name: "run", command: new URL("foo", import.meta.url) })).state, + runBar: (await Deno.permissions.query({ name: "run", command: "bar" })).state, + runBaz: (await Deno.permissions.query({ name: "run", command: "./baz" })).state, + runAbsent: (await Deno.permissions.query({ name: "run", command: "absent" })).state, + writeGlobal: (await Deno.permissions.query({ name: "write" })).state, + writeFoo: (await Deno.permissions.query({ name: "write", path: new URL("foo", import.meta.url) })).state, + writeBar: (await Deno.permissions.query({ name: "write", path: "bar" })).state, + writeAbsent: (await Deno.permissions.query({ name: "write", path: "absent" })).state, +}); diff --git a/cli/tests/testdata/workers/test.ts b/cli/tests/testdata/workers/test.ts index effd104f5a..4a6575863e 100644 --- a/cli/tests/testdata/workers/test.ts +++ b/cli/tests/testdata/workers/test.ts @@ -8,7 +8,6 @@ import { assertThrows, } from "../../../../test_util/std/testing/asserts.ts"; import { deferred } from "../../../../test_util/std/async/deferred.ts"; -import { fromFileUrl } from "../../../../test_util/std/path/mod.ts"; Deno.test({ name: "worker terminate", @@ -454,7 +453,6 @@ Deno.test("Worker limit children permissions", async function () { }); Deno.test("Worker limit children permissions granularly", async function () { - const promise = deferred(); const worker = new Worker( new URL("./read_check_granular_worker.js", import.meta.url).href, { @@ -462,53 +460,52 @@ Deno.test("Worker limit children permissions granularly", async function () { deno: { namespace: true, permissions: { - read: [ - new URL("./read_check_worker.js", import.meta.url), - ], + env: ["foo"], + hrtime: true, + net: ["foo", "bar:8000"], + ffi: [new URL("foo", import.meta.url), "bar"], + read: [new URL("foo", import.meta.url), "bar"], + run: [new URL("foo", import.meta.url), "bar", "./baz"], + write: [new URL("foo", import.meta.url), "bar"], }, }, }, ); - - //Routes are relative to the spawned worker location - const routes = [ - { - permission: false, - path: fromFileUrl( - new URL("read_check_granular_worker.js", import.meta.url), - ), - }, - { - permission: true, - path: fromFileUrl(new URL("read_check_worker.js", import.meta.url)), - }, - ]; - - let checked = 0; - worker.onmessage = ({ data }) => { - checked++; - assertEquals(data.hasPermission, routes[data.index].permission); - routes.shift(); - if (checked === routes.length) { - promise.resolve(); - } - }; - - routes.forEach(({ path }, index) => - worker.postMessage({ - index, - path, - }) - ); - - await promise; + const promise = deferred(); + worker.onmessage = ({ data }) => promise.resolve(data); + assertEquals(await promise, { + envGlobal: "prompt", + envFoo: "granted", + envAbsent: "prompt", + hrtime: "granted", + netGlobal: "prompt", + netFoo: "granted", + netFoo8000: "granted", + netBar: "prompt", + netBar8000: "granted", + ffiGlobal: "prompt", + ffiFoo: "granted", + ffiBar: "granted", + ffiAbsent: "prompt", + readGlobal: "prompt", + readFoo: "granted", + readBar: "granted", + readAbsent: "prompt", + runGlobal: "prompt", + runFoo: "granted", + runBar: "granted", + runBaz: "granted", + runAbsent: "prompt", + writeGlobal: "prompt", + writeFoo: "granted", + writeBar: "granted", + writeAbsent: "prompt", + }); worker.terminate(); }); Deno.test("Nested worker limit children permissions", async function () { - const promise = deferred(); - - /** This worker has read permissions but doesn't grant them to its children */ + /** This worker has permissions but doesn't grant them to its children */ const worker = new Worker( new URL("./parent_read_check_worker.js", import.meta.url).href, { @@ -519,104 +516,65 @@ Deno.test("Nested worker limit children permissions", async function () { }, }, ); - - worker.onmessage = ({ data }) => { - assert(data.parentHasPermission); - assert(!data.childHasPermission); - promise.resolve(); - }; - - worker.postMessage(null); - - await promise; - worker.terminate(); -}); - -Deno.test("Nested worker limit children permissions granularly", async function () { const promise = deferred(); - - /** This worker has read permissions but doesn't grant them to its children */ - const worker = new Worker( - new URL("./parent_read_check_granular_worker.js", import.meta.url) - .href, - { - type: "module", - deno: { - namespace: true, - permissions: { - read: [ - new URL("./read_check_granular_worker.js", import.meta.url), - ], - }, - }, - }, - ); - - //Routes are relative to the spawned worker location - const routes = [ - { - childHasPermission: false, - parentHasPermission: true, - path: fromFileUrl( - new URL("read_check_granular_worker.js", import.meta.url), - ), - }, - { - childHasPermission: false, - parentHasPermission: false, - path: fromFileUrl(new URL("read_check_worker.js", import.meta.url)), - }, - ]; - - let checked = 0; - worker.onmessage = ({ data }) => { - checked++; - assertEquals( - data.childHasPermission, - routes[data.index].childHasPermission, - ); - assertEquals( - data.parentHasPermission, - routes[data.index].parentHasPermission, - ); - if (checked === routes.length) { - promise.resolve(); - } - }; - - // Index needed cause requests will be handled asynchronously - routes.forEach(({ path }, index) => - worker.postMessage({ - index, - path, - }) - ); - - await promise; + worker.onmessage = ({ data }) => promise.resolve(data); + assertEquals(await promise, { + envGlobal: "prompt", + envFoo: "prompt", + envAbsent: "prompt", + hrtime: "prompt", + netGlobal: "prompt", + netFoo: "prompt", + netFoo8000: "prompt", + netBar: "prompt", + netBar8000: "prompt", + ffiGlobal: "prompt", + ffiFoo: "prompt", + ffiBar: "prompt", + ffiAbsent: "prompt", + readGlobal: "prompt", + readFoo: "prompt", + readBar: "prompt", + readAbsent: "prompt", + runGlobal: "prompt", + runFoo: "prompt", + runBar: "prompt", + runBaz: "prompt", + runAbsent: "prompt", + writeGlobal: "prompt", + writeFoo: "prompt", + writeBar: "prompt", + writeAbsent: "prompt", + }); worker.terminate(); }); // This test relies on env permissions not being granted on main thread -Deno.test("Worker initialization throws on worker permissions greater than parent thread permissions", function () { - assertThrows( - () => { - const worker = new Worker( - new URL("./deno_worker.ts", import.meta.url).href, - { - type: "module", - deno: { - namespace: true, - permissions: { - env: true, +Deno.test({ + name: + "Worker initialization throws on worker permissions greater than parent thread permissions", + permissions: { env: false }, + fn: function () { + assertThrows( + () => { + const worker = new Worker( + new URL("./deno_worker.ts", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: { + env: true, + }, }, }, - }, - ); - worker.terminate(); - }, - Deno.errors.PermissionDenied, - "Can't escalate parent thread permissions", - ); + ); + worker.terminate(); + }, + Deno.errors.PermissionDenied, + "Can't escalate parent thread permissions", + ); + }, }); Deno.test("Worker with disabled permissions", async function () { @@ -643,6 +601,19 @@ Deno.test("Worker with disabled permissions", async function () { worker.terminate(); }); +Deno.test("Worker with invalid permission arg", function () { + assertThrows( + () => + new Worker(`data:,close();`, { + type: "module", + // @ts-expect-error invalid env value + deno: { permissions: { env: "foo" } }, + }), + TypeError, + 'Error parsing args: (deno.permissions.env) invalid value: string "foo", expected "inherit" or boolean or string[]', + ); +}); + Deno.test({ name: "worker location", fn: async function () { diff --git a/ext/ffi/lib.rs b/ext/ffi/lib.rs index 4b83070d3a..f1e593296a 100644 --- a/ext/ffi/lib.rs +++ b/ext/ffi/lib.rs @@ -20,6 +20,8 @@ use std::cell::RefCell; use std::collections::HashMap; use std::convert::TryFrom; use std::ffi::c_void; +use std::path::Path; +use std::path::PathBuf; use std::rc::Rc; pub struct Unstable(pub bool); @@ -37,7 +39,7 @@ fn check_unstable(state: &OpState, api_name: &str) { } pub trait FfiPermissions { - fn check(&mut self, path: &str) -> Result<(), AnyError>; + fn check(&mut self, path: &Path) -> Result<(), AnyError>; } #[derive(Clone)] @@ -366,7 +368,7 @@ where check_unstable(state, "Deno.dlopen"); let permissions = state.borrow_mut::(); - permissions.check(&path)?; + permissions.check(&PathBuf::from(&path))?; let lib = Library::open(&path).map_err(|e| { dlopen::Error::OpeningLibraryError(std::io::Error::new( diff --git a/runtime/build.rs b/runtime/build.rs index 920aafc9f1..b1d4fa8cba 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -83,7 +83,10 @@ mod not_docs { } impl deno_ffi::FfiPermissions for Permissions { - fn check(&mut self, _path: &str) -> Result<(), deno_core::error::AnyError> { + fn check( + &mut self, + _path: &Path, + ) -> Result<(), deno_core::error::AnyError> { unreachable!("snapshotting!") } } diff --git a/runtime/js/40_permissions.js b/runtime/js/10_permissions.js similarity index 83% rename from runtime/js/40_permissions.js rename to runtime/js/10_permissions.js index 147ae92f04..a6884aab9a 100644 --- a/runtime/js/40_permissions.js +++ b/runtime/js/10_permissions.js @@ -10,7 +10,10 @@ } = window; const { pathFromURL } = window.__bootstrap.util; const { + ArrayIsArray, ArrayPrototypeIncludes, + ArrayPrototypeMap, + ArrayPrototypeSlice, Map, MapPrototypeGet, MapPrototypeHas, @@ -162,7 +165,9 @@ ); } - if (desc.name === "read" || desc.name === "write") { + if ( + desc.name === "read" || desc.name === "write" || desc.name === "ffi" + ) { desc.path = pathFromURL(desc.path); } else if (desc.name === "run") { desc.command = pathFromURL(desc.command); @@ -213,7 +218,34 @@ const permissions = new Permissions(illegalConstructorKey); + /** Converts all file URLs in FS allowlists to paths. */ + function serializePermissions(permissions) { + if (typeof permissions == "object" && permissions != null) { + const serializedPermissions = {}; + for (const key of ["read", "write", "run", "ffi"]) { + if (ArrayIsArray(permissions[key])) { + serializedPermissions[key] = ArrayPrototypeMap( + permissions[key], + (path) => pathFromURL(path), + ); + } else { + serializedPermissions[key] = permissions[key]; + } + } + for (const key of ["env", "hrtime", "net"]) { + if (ArrayIsArray(permissions[key])) { + serializedPermissions[key] = ArrayPrototypeSlice(permissions[key]); + } else { + serializedPermissions[key] = permissions[key]; + } + } + return serializedPermissions; + } + return permissions; + } + window.__bootstrap.permissions = { + serializePermissions, permissions, Permissions, PermissionStatus, diff --git a/runtime/js/11_workers.js b/runtime/js/11_workers.js index 53b2563347..8f0095056e 100644 --- a/runtime/js/11_workers.js +++ b/runtime/js/11_workers.js @@ -4,8 +4,6 @@ ((window) => { const core = window.Deno.core; const { - ArrayIsArray, - ArrayPrototypeMap, Error, StringPrototypeStartsWith, String, @@ -15,7 +13,8 @@ const webidl = window.__bootstrap.webidl; const { URL } = window.__bootstrap.url; const { getLocationHref } = window.__bootstrap.location; - const { log, pathFromURL } = window.__bootstrap.util; + const { serializePermissions } = window.__bootstrap.permissions; + const { log } = window.__bootstrap.util; const { defineEventHandler } = window.__bootstrap.event; const { deserializeJsMessageData, serializeJsMessageData } = window.__bootstrap.messagePort; @@ -32,7 +31,7 @@ return core.opSync("op_create_worker", { hasSourceCode, name, - permissions, + permissions: serializePermissions(permissions), sourceCode, specifier, useDenoNamespace, @@ -56,87 +55,6 @@ return core.opAsync("op_host_recv_message", id); } - /** - * @param {"inherit" | boolean} value - * @param {string} permission - * @return {boolean} - */ - function parseUnitPermission( - value, - permission, - ) { - if (value !== "inherit" && typeof value !== "boolean") { - throw new Error( - `Expected 'boolean' for ${permission} permission, ${typeof value} received`, - ); - } - return value === "inherit" ? undefined : value; - } - - /** - * @param {string} permission - * @return {(boolean | string[])} - */ - function parseArrayPermission( - value, - permission, - ) { - if (typeof value === "string") { - if (value !== "inherit") { - throw new Error( - `Expected 'array' or 'boolean' for ${permission} permission, "${value}" received`, - ); - } - } else if (!ArrayIsArray(value) && typeof value !== "boolean") { - throw new Error( - `Expected 'array' or 'boolean' for ${permission} permission, ${typeof value} received`, - ); - //Casts URLs to absolute routes - } else if (ArrayIsArray(value)) { - value = ArrayPrototypeMap(value, (route) => { - if (route instanceof URL) { - if (permission === "net") { - throw new Error( - `Expected 'string' for net permission, received 'URL'`, - ); - } else if (permission === "env") { - throw new Error( - `Expected 'string' for env permission, received 'URL'`, - ); - } else { - route = pathFromURL(route); - } - } - return route; - }); - } - - return value === "inherit" ? undefined : value; - } - - /** - * Normalizes data, runs checks on parameters and deletes inherited permissions - */ - function parsePermissions({ - env = "inherit", - hrtime = "inherit", - net = "inherit", - ffi = "inherit", - read = "inherit", - run = "inherit", - write = "inherit", - }) { - return { - env: parseArrayPermission(env, "env"), - hrtime: parseUnitPermission(hrtime, "hrtime"), - net: parseArrayPermission(net, "net"), - ffi: parseUnitPermission(ffi, "ffi"), - read: parseArrayPermission(read, "read"), - run: parseUnitPermission(run, "run"), - write: parseArrayPermission(write, "write"), - }; - } - class Worker extends EventTarget { #id = 0; #name = ""; @@ -152,43 +70,23 @@ super(); specifier = String(specifier); const { - deno = {}, - name = "unknown", + deno, + name, type = "classic", } = options; - // TODO(Soremwar) - // `deno: boolean` is kept for backwards compatibility with the previous - // worker options implementation. Remove for 2.0 - let workerDenoAttributes; - if (typeof deno == "boolean") { - workerDenoAttributes = { - // Change this to enable the Deno namespace by default - namespace: deno, - permissions: null, - }; + let namespace; + let permissions; + if (typeof deno == "object") { + namespace = deno.namespace ?? false; + permissions = deno.permissions ?? undefined; } else { - workerDenoAttributes = { - // Change this to enable the Deno namespace by default - namespace: !!(deno?.namespace ?? false), - permissions: (deno?.permissions ?? "inherit") === "inherit" - ? null - : deno?.permissions, - }; - - // If the permission option is set to "none", all permissions - // must be removed from the worker - if (workerDenoAttributes.permissions === "none") { - workerDenoAttributes.permissions = { - env: false, - hrtime: false, - net: false, - ffi: false, - read: false, - run: false, - write: false, - }; - } + // Assume `deno: boolean | undefined`. + // TODO(Soremwar) + // `deno: boolean` is kept for backwards compatibility with the previous + // worker options implementation. Remove for 2.0 + namespace = !!deno; + permissions = undefined; } const workerType = webidl.converters["WorkerType"](type); @@ -218,17 +116,16 @@ specifier, hasSourceCode, sourceCode, - workerDenoAttributes.namespace, - workerDenoAttributes.permissions === null - ? null - : parsePermissions(workerDenoAttributes.permissions), - options?.name, + namespace, + permissions, + name, workerType, ); this.#id = id; this.#pollControl(); this.#pollMessages(); } + #handleError(e) { const event = new ErrorEvent("error", { cancelable: true, @@ -359,7 +256,6 @@ ]); window.__bootstrap.worker = { - parsePermissions, Worker, }; })(this); diff --git a/runtime/js/40_testing.js b/runtime/js/40_testing.js index 5b404766ea..b4f7c847cb 100644 --- a/runtime/js/40_testing.js +++ b/runtime/js/40_testing.js @@ -3,10 +3,10 @@ ((window) => { const core = window.Deno.core; - const { parsePermissions } = window.__bootstrap.worker; const { setExitHandler } = window.__bootstrap.os; const { Console, inspectArgs } = window.__bootstrap.console; const { metrics } = core; + const { serializePermissions } = window.__bootstrap.permissions; const { assert } = window.__bootstrap.util; const { ArrayPrototypeFilter, @@ -230,7 +230,7 @@ finishing test case.`; function pledgePermissions(permissions) { return core.opSync( "op_pledge_test_permissions", - parsePermissions(permissions), + serializePermissions(permissions), ); } @@ -289,7 +289,7 @@ finishing test case.`; if (testDef.permissions) { testDef.fn = withPermissions( testDef.fn, - parsePermissions(testDef.permissions), + testDef.permissions, ); } diff --git a/runtime/ops/permissions.rs b/runtime/ops/permissions.rs index d9f341633c..05f4b75d7c 100644 --- a/runtime/ops/permissions.rs +++ b/runtime/ops/permissions.rs @@ -28,7 +28,6 @@ pub struct PermissionArgs { host: Option, variable: Option, command: Option, - library: Option, } pub fn op_query_permission( @@ -50,7 +49,7 @@ pub fn op_query_permission( ), "env" => permissions.env.query(args.variable.as_deref()), "run" => permissions.run.query(args.command.as_deref()), - "ffi" => permissions.ffi.query(args.library.as_deref()), + "ffi" => permissions.ffi.query(args.path.as_deref().map(Path::new)), "hrtime" => permissions.hrtime.query(), n => { return Err(custom_error( @@ -81,7 +80,7 @@ pub fn op_revoke_permission( ), "env" => permissions.env.revoke(args.variable.as_deref()), "run" => permissions.run.revoke(args.command.as_deref()), - "ffi" => permissions.ffi.revoke(args.library.as_deref()), + "ffi" => permissions.ffi.revoke(args.path.as_deref().map(Path::new)), "hrtime" => permissions.hrtime.revoke(), n => { return Err(custom_error( @@ -112,7 +111,7 @@ pub fn op_request_permission( ), "env" => permissions.env.request(args.variable.as_deref()), "run" => permissions.run.request(args.command.as_deref()), - "ffi" => permissions.ffi.request(args.library.as_deref()), + "ffi" => permissions.ffi.request(args.path.as_deref().map(Path::new)), "hrtime" => permissions.hrtime.request(), n => { return Err(custom_error( diff --git a/runtime/ops/worker_host.rs b/runtime/ops/worker_host.rs index c1ce782da3..e9eb380e05 100644 --- a/runtime/ops/worker_host.rs +++ b/runtime/ops/worker_host.rs @@ -1,18 +1,9 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use crate::ops::TestingFeaturesEnabled; -use crate::permissions::resolve_read_allowlist; -use crate::permissions::resolve_write_allowlist; -use crate::permissions::EnvDescriptor; -use crate::permissions::FfiDescriptor; -use crate::permissions::NetDescriptor; -use crate::permissions::PermissionState; +use crate::permissions::create_child_permissions; +use crate::permissions::ChildPermissionsArg; use crate::permissions::Permissions; -use crate::permissions::ReadDescriptor; -use crate::permissions::RunDescriptor; -use crate::permissions::UnaryPermission; -use crate::permissions::UnitPermission; -use crate::permissions::WriteDescriptor; use crate::web_worker::run_web_worker; use crate::web_worker::SendableWebWorkerHandle; use crate::web_worker::WebWorker; @@ -20,14 +11,10 @@ use crate::web_worker::WebWorkerHandle; use crate::web_worker::WebWorkerType; use crate::web_worker::WorkerControlEvent; use crate::web_worker::WorkerId; -use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::op_async; use deno_core::op_sync; -use deno_core::serde::de; -use deno_core::serde::de::SeqAccess; use deno_core::serde::Deserialize; -use deno_core::serde::Deserializer; use deno_core::Extension; use deno_core::ModuleSpecifier; use deno_core::OpState; @@ -35,10 +22,6 @@ use deno_web::JsMessageData; use log::debug; use std::cell::RefCell; use std::collections::HashMap; -use std::collections::HashSet; -use std::convert::From; -use std::fmt; -use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use std::thread::JoinHandle; @@ -131,369 +114,12 @@ pub fn init(create_web_worker_cb: Arc) -> Extension { .build() } -fn merge_boolean_permission( - mut main: UnitPermission, - worker: Option, -) -> Result { - if let Some(worker) = worker { - if worker < main.state { - return Err(custom_error( - "PermissionDenied", - "Can't escalate parent thread permissions", - )); - } else { - main.state = worker; - } - } - Ok(main) -} - -fn merge_net_permission( - mut main: UnaryPermission, - worker: Option>, -) -> Result, AnyError> { - if let Some(worker) = worker { - if (worker.global_state < main.global_state) - || !worker - .granted_list - .iter() - .all(|x| main.check(&(&x.0, x.1)).is_ok()) - { - return Err(custom_error( - "PermissionDenied", - "Can't escalate parent thread permissions", - )); - } else { - main.global_state = worker.global_state; - main.granted_list = worker.granted_list; - } - } - Ok(main) -} - -fn merge_read_permission( - mut main: UnaryPermission, - worker: Option>, -) -> Result, AnyError> { - if let Some(worker) = worker { - if (worker.global_state < main.global_state) - || !worker - .granted_list - .iter() - .all(|x| main.check(x.0.as_path()).is_ok()) - { - return Err(custom_error( - "PermissionDenied", - "Can't escalate parent thread permissions", - )); - } else { - main.global_state = worker.global_state; - main.granted_list = worker.granted_list; - } - } - Ok(main) -} - -fn merge_write_permission( - mut main: UnaryPermission, - worker: Option>, -) -> Result, AnyError> { - if let Some(worker) = worker { - if (worker.global_state < main.global_state) - || !worker - .granted_list - .iter() - .all(|x| main.check(x.0.as_path()).is_ok()) - { - return Err(custom_error( - "PermissionDenied", - "Can't escalate parent thread permissions", - )); - } else { - main.global_state = worker.global_state; - main.granted_list = worker.granted_list; - } - } - Ok(main) -} - -fn merge_env_permission( - mut main: UnaryPermission, - worker: Option>, -) -> Result, AnyError> { - if let Some(worker) = worker { - if (worker.global_state < main.global_state) - || !worker - .granted_list - .iter() - .all(|x| main.check(x.as_ref()).is_ok()) - { - return Err(custom_error( - "PermissionDenied", - "Can't escalate parent thread permissions", - )); - } else { - main.global_state = worker.global_state; - main.granted_list = worker.granted_list; - } - } - Ok(main) -} - -fn merge_run_permission( - mut main: UnaryPermission, - worker: Option>, -) -> Result, AnyError> { - if let Some(worker) = worker { - if (worker.global_state < main.global_state) - || !worker.granted_list.iter().all(|x| main.check(&x.0).is_ok()) - { - return Err(custom_error( - "PermissionDenied", - "Can't escalate parent thread permissions", - )); - } else { - main.global_state = worker.global_state; - main.granted_list = worker.granted_list; - } - } - Ok(main) -} - -fn merge_ffi_permission( - mut main: UnaryPermission, - worker: Option>, -) -> Result, AnyError> { - if let Some(worker) = worker { - if (worker.global_state < main.global_state) - || !worker.granted_list.iter().all(|x| main.check(&x.0).is_ok()) - { - return Err(custom_error( - "PermissionDenied", - "Can't escalate parent thread permissions", - )); - } else { - main.global_state = worker.global_state; - main.granted_list = worker.granted_list; - } - } - Ok(main) -} - -pub fn create_worker_permissions( - main_perms: Permissions, - worker_perms: PermissionsArg, -) -> Result { - Ok(Permissions { - env: merge_env_permission(main_perms.env, worker_perms.env)?, - hrtime: merge_boolean_permission(main_perms.hrtime, worker_perms.hrtime)?, - net: merge_net_permission(main_perms.net, worker_perms.net)?, - ffi: merge_ffi_permission(main_perms.ffi, worker_perms.ffi)?, - read: merge_read_permission(main_perms.read, worker_perms.read)?, - run: merge_run_permission(main_perms.run, worker_perms.run)?, - write: merge_write_permission(main_perms.write, worker_perms.write)?, - }) -} - -#[derive(Debug, Deserialize)] -pub struct PermissionsArg { - #[serde(default, deserialize_with = "as_unary_env_permission")] - env: Option>, - #[serde(default, deserialize_with = "as_permission_state")] - hrtime: Option, - #[serde(default, deserialize_with = "as_unary_net_permission")] - net: Option>, - #[serde(default, deserialize_with = "as_unary_ffi_permission")] - ffi: Option>, - #[serde(default, deserialize_with = "as_unary_read_permission")] - read: Option>, - #[serde(default, deserialize_with = "as_unary_run_permission")] - run: Option>, - #[serde(default, deserialize_with = "as_unary_write_permission")] - write: Option>, -} - -fn as_permission_state<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value: bool = Deserialize::deserialize(deserializer)?; - - match value { - true => Ok(Some(PermissionState::Granted)), - false => Ok(Some(PermissionState::Denied)), - } -} - -struct UnaryPermissionBase { - global_state: PermissionState, - paths: Vec, -} - -struct ParseBooleanOrStringVec; - -impl<'de> de::Visitor<'de> for ParseBooleanOrStringVec { - type Value = UnaryPermissionBase; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a vector of strings or a boolean") - } - - // visit_unit maps undefined/missing values to false - fn visit_unit(self) -> Result - where - E: de::Error, - { - self.visit_bool(false) - } - - fn visit_bool(self, v: bool) -> Result - where - E: de::Error, - { - Ok(UnaryPermissionBase { - global_state: match v { - true => PermissionState::Granted, - false => PermissionState::Denied, - }, - paths: Vec::new(), - }) - } - - fn visit_seq(self, mut visitor: V) -> Result - where - V: SeqAccess<'de>, - { - let mut vec: Vec = Vec::new(); - - let mut value = visitor.next_element::()?; - while value.is_some() { - vec.push(value.unwrap()); - value = visitor.next_element()?; - } - Ok(UnaryPermissionBase { - global_state: PermissionState::Prompt, - paths: vec, - }) - } -} - -fn as_unary_net_permission<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let value: UnaryPermissionBase = - deserializer.deserialize_any(ParseBooleanOrStringVec)?; - - let allowed: HashSet = value - .paths - .into_iter() - .map(NetDescriptor::from_string) - .collect(); - - Ok(Some(UnaryPermission:: { - global_state: value.global_state, - granted_list: allowed, - ..Default::default() - })) -} - -fn as_unary_read_permission<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let value: UnaryPermissionBase = - deserializer.deserialize_any(ParseBooleanOrStringVec)?; - - let paths: Vec = - value.paths.into_iter().map(PathBuf::from).collect(); - - Ok(Some(UnaryPermission:: { - global_state: value.global_state, - granted_list: resolve_read_allowlist(&Some(paths)), - ..Default::default() - })) -} - -fn as_unary_write_permission<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let value: UnaryPermissionBase = - deserializer.deserialize_any(ParseBooleanOrStringVec)?; - - let paths: Vec = - value.paths.into_iter().map(PathBuf::from).collect(); - - Ok(Some(UnaryPermission:: { - global_state: value.global_state, - granted_list: resolve_write_allowlist(&Some(paths)), - ..Default::default() - })) -} - -fn as_unary_env_permission<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let value: UnaryPermissionBase = - deserializer.deserialize_any(ParseBooleanOrStringVec)?; - - Ok(Some(UnaryPermission:: { - global_state: value.global_state, - granted_list: value.paths.into_iter().map(EnvDescriptor::new).collect(), - ..Default::default() - })) -} - -fn as_unary_run_permission<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let value: UnaryPermissionBase = - deserializer.deserialize_any(ParseBooleanOrStringVec)?; - - Ok(Some(UnaryPermission:: { - global_state: value.global_state, - granted_list: value.paths.into_iter().map(RunDescriptor).collect(), - ..Default::default() - })) -} - -fn as_unary_ffi_permission<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let value: UnaryPermissionBase = - deserializer.deserialize_any(ParseBooleanOrStringVec)?; - - Ok(Some(UnaryPermission:: { - global_state: value.global_state, - granted_list: value.paths.into_iter().map(FfiDescriptor).collect(), - ..Default::default() - })) -} - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateWorkerArgs { has_source_code: bool, name: Option, - permissions: Option, + permissions: Option, source_code: String, specifier: String, use_deno_namespace: bool, @@ -528,13 +154,18 @@ fn op_create_worker( ); } } - let parent_permissions = state.borrow::().clone(); - let worker_permissions = if let Some(permissions) = args.permissions { + + if args.permissions.is_some() { super::check_unstable(state, "Worker.deno.permissions"); - create_worker_permissions(parent_permissions.clone(), permissions)? + } + let parent_permissions = state.borrow_mut::(); + let worker_permissions = if let Some(child_permissions_arg) = args.permissions + { + create_child_permissions(parent_permissions, child_permissions_arg)? } else { parent_permissions.clone() }; + let parent_permissions = parent_permissions.clone(); let worker_id = state.take::(); let create_module_loader = state.take::(); diff --git a/runtime/permissions.rs b/runtime/permissions.rs index 081268aa9a..e62ce4eb13 100644 --- a/runtime/permissions.rs +++ b/runtime/permissions.rs @@ -7,8 +7,11 @@ use deno_core::error::uri_error; use deno_core::error::AnyError; #[cfg(test)] use deno_core::parking_lot::Mutex; +use deno_core::serde::de; use deno_core::serde::Deserialize; +use deno_core::serde::Deserializer; use deno_core::serde::Serialize; +use deno_core::serde_json; use deno_core::url; use deno_core::ModuleSpecifier; use deno_core::OpState; @@ -17,6 +20,8 @@ use std::collections::HashSet; use std::fmt; use std::hash::Hash; use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::string::ToString; #[cfg(test)] use std::sync::atomic::AtomicBool; #[cfg(test)] @@ -115,7 +120,7 @@ impl Default for PermissionState { } } -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct UnitPermission { pub name: &'static str, pub description: &'static str, @@ -161,7 +166,7 @@ impl UnitPermission { /// A normalized environment variable name. On Windows this will /// be uppercase and on other platforms it will stay as-is. -#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] +#[derive(Clone, Eq, PartialEq, Hash, Debug)] struct EnvVarName { inner: String, } @@ -184,7 +189,7 @@ impl AsRef for EnvVarName { } } -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct UnaryPermission { pub name: &'static str, pub description: &'static str, @@ -194,13 +199,13 @@ pub struct UnaryPermission { pub prompt: bool, } -#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] +#[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct ReadDescriptor(pub PathBuf); -#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] +#[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct WriteDescriptor(pub PathBuf); -#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] +#[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct NetDescriptor(pub String, pub Option); impl NetDescriptor { @@ -225,7 +230,7 @@ impl fmt::Display for NetDescriptor { } } -#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] +#[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct EnvDescriptor(EnvVarName); impl EnvDescriptor { @@ -240,11 +245,38 @@ impl AsRef for EnvDescriptor { } } -#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] -pub struct RunDescriptor(pub String); +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub enum RunDescriptor { + Name(String), + Path(PathBuf), +} -#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] -pub struct FfiDescriptor(pub String); +impl FromStr for RunDescriptor { + type Err = (); + + fn from_str(s: &str) -> Result { + let is_path = s.contains('/'); + #[cfg(windows)] + let is_path = is_path || s.contains('\\') || Path::new(s).is_absolute(); + if is_path { + Ok(Self::Path(resolve_from_cwd(Path::new(s)).unwrap())) + } else { + Ok(Self::Name(s.to_string())) + } + } +} + +impl ToString for RunDescriptor { + fn to_string(&self) -> String { + match self { + RunDescriptor::Name(s) => s.clone(), + RunDescriptor::Path(p) => p.to_string_lossy().to_string(), + } + } +} + +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct FfiDescriptor(pub PathBuf); impl UnaryPermission { pub fn query(&self, path: Option<&Path>) -> PermissionState { @@ -369,6 +401,32 @@ impl UnaryPermission { } result } + + pub fn check_all(&mut self) -> Result<(), AnyError> { + let (result, prompted) = + self.query(None).check(self.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, + } + } } impl UnaryPermission { @@ -470,6 +528,32 @@ impl UnaryPermission { } result } + + pub fn check_all(&mut self) -> Result<(), AnyError> { + let (result, prompted) = + self.query(None).check(self.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: "write", + description: "write to the file system", + global_state: Default::default(), + granted_list: Default::default(), + denied_list: Default::default(), + prompt: false, + } + } } impl UnaryPermission { @@ -615,6 +699,34 @@ impl UnaryPermission { } result } + + pub fn check_all(&mut self) -> Result<(), AnyError> { + let (result, prompted) = + self + .query::<&str>(None) + .check(self.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: "net", + description: "network", + global_state: Default::default(), + granted_list: Default::default(), + denied_list: Default::default(), + prompt: false, + } + } } impl UnaryPermission { @@ -623,14 +735,14 @@ impl UnaryPermission { if self.global_state == PermissionState::Denied && match env.as_ref() { None => true, - Some(env) => self.denied_list.iter().any(|env_| &env_.0 == env), + 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.iter().any(|env_| &env_.0 == env), + Some(env) => self.granted_list.contains(&EnvDescriptor::new(env)), } { PermissionState::Granted @@ -717,19 +829,36 @@ impl UnaryPermission { } } +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, + } + } +} + 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.iter().any(|cmd_| cmd_.0 == cmd), + 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.iter().any(|cmd_| cmd_.0 == cmd), + Some(cmd) => self + .granted_list + .contains(&RunDescriptor::from_str(cmd).unwrap()), } { PermissionState::Granted @@ -743,15 +872,21 @@ impl UnaryPermission { let state = self.query(Some(cmd)); if state == PermissionState::Prompt { if permission_prompt(&format!("run access to \"{}\"", cmd)) { - self.granted_list.insert(RunDescriptor(cmd.to_string())); + self + .granted_list + .insert(RunDescriptor::from_str(cmd).unwrap()); PermissionState::Granted } else { - self.denied_list.insert(RunDescriptor(cmd.to_string())); + self + .denied_list + .insert(RunDescriptor::from_str(cmd).unwrap()); self.global_state = PermissionState::Denied; PermissionState::Denied } } else if state == PermissionState::Granted { - self.granted_list.insert(RunDescriptor(cmd.to_string())); + self + .granted_list + .insert(RunDescriptor::from_str(cmd).unwrap()); PermissionState::Granted } else { state @@ -775,7 +910,9 @@ impl UnaryPermission { pub fn revoke(&mut self, cmd: Option<&str>) -> PermissionState { if let Some(cmd) = cmd { - self.granted_list.remove(&RunDescriptor(cmd.to_string())); + self + .granted_list + .remove(&RunDescriptor::from_str(cmd).unwrap()); } else { self.granted_list.clear(); } @@ -793,9 +930,13 @@ impl UnaryPermission { ); if prompted { if result.is_ok() { - self.granted_list.insert(RunDescriptor(cmd.to_string())); + self + .granted_list + .insert(RunDescriptor::from_str(cmd).unwrap()); } else { - self.denied_list.insert(RunDescriptor(cmd.to_string())); + self + .denied_list + .insert(RunDescriptor::from_str(cmd).unwrap()); self.global_state = PermissionState::Denied; } } @@ -816,19 +957,33 @@ impl UnaryPermission { } } +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, + } + } +} + impl UnaryPermission { - pub fn query(&self, lib: Option<&str>) -> PermissionState { + 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 lib { + && match path.as_ref() { None => true, - Some(lib) => self.denied_list.iter().any(|lib_| lib_.0 == lib), + Some(path) => self.denied_list.contains(&FfiDescriptor(path.clone())), } { PermissionState::Denied } else if self.global_state == PermissionState::Granted - || match lib { + || match path.as_ref() { None => false, - Some(lib) => self.granted_list.iter().any(|lib_| lib_.0 == lib), + Some(path) => self.granted_list.contains(&FfiDescriptor(path.clone())), } { PermissionState::Granted @@ -837,20 +992,24 @@ impl UnaryPermission { } } - pub fn request(&mut self, lib: Option<&str>) -> PermissionState { - if let Some(lib) = lib { - let state = self.query(Some(lib)); + 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 { - if permission_prompt(&format!("ffi access to \"{}\"", lib)) { - self.granted_list.insert(FfiDescriptor(lib.to_string())); + if permission_prompt(&format!( + "ffi access to \"{}\"", + display_path.display() + )) { + self.granted_list.insert(FfiDescriptor(resolved_path)); PermissionState::Granted } else { - self.denied_list.insert(FfiDescriptor(lib.to_string())); + self.denied_list.insert(FfiDescriptor(resolved_path)); self.global_state = PermissionState::Denied; PermissionState::Denied } } else if state == PermissionState::Granted { - self.granted_list.insert(FfiDescriptor(lib.to_string())); + self.granted_list.insert(FfiDescriptor(resolved_path)); PermissionState::Granted } else { state @@ -872,29 +1031,31 @@ impl UnaryPermission { } } - pub fn revoke(&mut self, lib: Option<&str>) -> PermissionState { - if let Some(lib) = lib { - self.granted_list.remove(&FfiDescriptor(lib.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.remove(&FfiDescriptor(path)); } else { self.granted_list.clear(); } if self.global_state == PermissionState::Granted { self.global_state = PermissionState::Prompt; } - self.query(lib) + self.query(path) } - pub fn check(&mut self, lib: &str) -> Result<(), AnyError> { - let (result, prompted) = self.query(Some(lib)).check( + pub fn check(&mut self, path: &Path) -> Result<(), AnyError> { + let (resolved_path, display_path) = resolved_and_display_path(path); + let (result, prompted) = self.query(Some(&resolved_path)).check( self.name, - Some(&format!("\"{}\"", lib)), + Some(&format!("\"{}\"", display_path.display())), self.prompt, ); if prompted { if result.is_ok() { - self.granted_list.insert(FfiDescriptor(lib.to_string())); + self.granted_list.insert(FfiDescriptor(resolved_path)); } else { - self.denied_list.insert(FfiDescriptor(lib.to_string())); + self.denied_list.insert(FfiDescriptor(resolved_path)); self.global_state = PermissionState::Denied; } } @@ -915,7 +1076,20 @@ impl UnaryPermission { } } -#[derive(Clone, Debug, Default, PartialEq)] +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, + } + } +} + +#[derive(Clone, Debug, PartialEq)] pub struct Permissions { pub read: UnaryPermission, pub write: UnaryPermission, @@ -926,12 +1100,26 @@ pub struct Permissions { pub hrtime: UnitPermission, } +impl Default for Permissions { + fn default() -> Self { + Self { + read: Permissions::new_read(&None, false), + write: Permissions::new_write(&None, false), + net: Permissions::new_net(&None, false), + env: Permissions::new_env(&None, false), + run: Permissions::new_run(&None, false), + ffi: Permissions::new_ffi(&None, false), + hrtime: Permissions::new_hrtime(false, false), + } + } +} + #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] pub struct PermissionsOptions { pub allow_env: Option>, pub allow_hrtime: bool, pub allow_net: Option>, - pub allow_ffi: Option>, + pub allow_ffi: Option>, pub allow_read: Option>, pub allow_run: Option>, pub allow_write: Option>, @@ -944,12 +1132,10 @@ impl Permissions { prompt: bool, ) -> UnaryPermission { UnaryPermission:: { - name: "read", - description: "read the file system", global_state: global_state_from_option(state), granted_list: resolve_read_allowlist(state), - denied_list: Default::default(), prompt, + ..Default::default() } } @@ -958,12 +1144,10 @@ impl Permissions { prompt: bool, ) -> UnaryPermission { UnaryPermission:: { - name: "write", - description: "write to the file system", global_state: global_state_from_option(state), granted_list: resolve_write_allowlist(state), - denied_list: Default::default(), prompt, + ..Default::default() } } @@ -972,8 +1156,6 @@ impl Permissions { prompt: bool, ) -> UnaryPermission { UnaryPermission:: { - name: "net", - description: "network", global_state: global_state_from_option(state), granted_list: state .as_ref() @@ -983,8 +1165,8 @@ impl Permissions { .collect() }) .unwrap_or_else(HashSet::new), - denied_list: Default::default(), prompt, + ..Default::default() } } @@ -993,15 +1175,13 @@ impl Permissions { prompt: bool, ) -> UnaryPermission { UnaryPermission:: { - name: "env", - description: "environment variables", global_state: global_state_from_option(state), granted_list: state .as_ref() .map(|v| v.iter().map(EnvDescriptor::new).collect()) .unwrap_or_else(HashSet::new), - denied_list: Default::default(), prompt, + ..Default::default() } } @@ -1010,32 +1190,29 @@ impl Permissions { prompt: bool, ) -> UnaryPermission { UnaryPermission:: { - name: "run", - description: "run a subprocess", global_state: global_state_from_option(state), granted_list: state .as_ref() - .map(|v| v.iter().map(|x| RunDescriptor(x.clone())).collect()) + .map(|v| { + v.iter() + .map(|x| RunDescriptor::from_str(x).unwrap()) + .collect() + }) .unwrap_or_else(HashSet::new), - denied_list: Default::default(), prompt, + ..Default::default() } } pub fn new_ffi( - state: &Option>, + state: &Option>, prompt: bool, ) -> UnaryPermission { UnaryPermission:: { - name: "ffi", - description: "load a dynamic library", global_state: global_state_from_option(state), - granted_list: state - .as_ref() - .map(|v| v.iter().map(|x| FfiDescriptor(x.clone())).collect()) - .unwrap_or_else(HashSet::new), - denied_list: Default::default(), + granted_list: resolve_ffi_allowlist(state), prompt, + ..Default::default() } } @@ -1137,7 +1314,7 @@ impl deno_websocket::WebSocketPermissions for Permissions { } impl deno_ffi::FfiPermissions for Permissions { - fn check(&mut self, path: &str) -> Result<(), AnyError> { + fn check(&mut self, path: &Path) -> Result<(), AnyError> { self.ffi.check(path) } } @@ -1196,6 +1373,20 @@ pub fn resolve_write_allowlist( } } +pub fn resolve_ffi_allowlist( + allow: &Option>, +) -> HashSet { + if let Some(v) = allow { + v.iter() + .map(|raw_path| { + FfiDescriptor(resolve_from_cwd(Path::new(&raw_path)).unwrap()) + }) + .collect() + } else { + HashSet::new() + } +} + /// Arbitrary helper. Resolves the path from CWD, and also gets a path that /// can be displayed without leaking the CWD when not allowed. fn resolved_and_display_path(path: &Path) -> (PathBuf, PathBuf) { @@ -1204,6 +1395,458 @@ fn resolved_and_display_path(path: &Path) -> (PathBuf, PathBuf) { (resolved_path, display_path) } +fn escalation_error() -> AnyError { + custom_error( + "PermissionDenied", + "Can't escalate parent thread permissions", + ) +} + +#[derive(Debug, PartialEq)] +pub enum ChildUnitPermissionArg { + Inherit, + Granted, + NotGranted, +} + +impl Default for ChildUnitPermissionArg { + fn default() -> Self { + ChildUnitPermissionArg::Inherit + } +} + +impl<'de> Deserialize<'de> for ChildUnitPermissionArg { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ChildUnitPermissionArgVisitor; + impl<'de> de::Visitor<'de> for ChildUnitPermissionArgVisitor { + type Value = ChildUnitPermissionArg; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("\"inherit\" or boolean") + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(ChildUnitPermissionArg::Inherit) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if v == "inherit" { + Ok(ChildUnitPermissionArg::Inherit) + } else { + Err(de::Error::invalid_value(de::Unexpected::Str(v), &self)) + } + } + + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + match v { + true => Ok(ChildUnitPermissionArg::Granted), + false => Ok(ChildUnitPermissionArg::NotGranted), + } + } + } + deserializer.deserialize_any(ChildUnitPermissionArgVisitor) + } +} + +#[derive(Debug, PartialEq)] +pub enum ChildUnaryPermissionArg { + Inherit, + Granted, + NotGranted, + GrantedList(Vec), +} + +impl Default for ChildUnaryPermissionArg { + fn default() -> Self { + ChildUnaryPermissionArg::Inherit + } +} + +impl<'de> Deserialize<'de> for ChildUnaryPermissionArg { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ChildUnaryPermissionArgVisitor; + impl<'de> de::Visitor<'de> for ChildUnaryPermissionArgVisitor { + type Value = ChildUnaryPermissionArg; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("\"inherit\" or boolean or string[]") + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(ChildUnaryPermissionArg::Inherit) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if v == "inherit" { + Ok(ChildUnaryPermissionArg::Inherit) + } else { + Err(de::Error::invalid_value(de::Unexpected::Str(v), &self)) + } + } + + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + match v { + true => Ok(ChildUnaryPermissionArg::Granted), + false => Ok(ChildUnaryPermissionArg::NotGranted), + } + } + + fn visit_seq( + self, + mut v: V, + ) -> Result + where + V: de::SeqAccess<'de>, + { + let mut granted_list = vec![]; + while let Some(value) = v.next_element::()? { + granted_list.push(value); + } + Ok(ChildUnaryPermissionArg::GrantedList(granted_list)) + } + } + deserializer.deserialize_any(ChildUnaryPermissionArgVisitor) + } +} + +/// Directly deserializable from JS worker and test permission options. +#[derive(Debug, Default, PartialEq)] +pub struct ChildPermissionsArg { + env: ChildUnaryPermissionArg, + hrtime: ChildUnitPermissionArg, + net: ChildUnaryPermissionArg, + ffi: ChildUnaryPermissionArg, + read: ChildUnaryPermissionArg, + run: ChildUnaryPermissionArg, + write: ChildUnaryPermissionArg, +} + +impl<'de> Deserialize<'de> for ChildPermissionsArg { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ChildPermissionsArgVisitor; + impl<'de> de::Visitor<'de> for ChildPermissionsArgVisitor { + type Value = ChildPermissionsArg; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("\"inherit\" or \"none\" or object") + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(ChildPermissionsArg::default()) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if v == "inherit" { + Ok(ChildPermissionsArg::default()) + } else if v == "none" { + Ok(ChildPermissionsArg { + env: ChildUnaryPermissionArg::NotGranted, + hrtime: ChildUnitPermissionArg::NotGranted, + net: ChildUnaryPermissionArg::NotGranted, + ffi: ChildUnaryPermissionArg::NotGranted, + read: ChildUnaryPermissionArg::NotGranted, + run: ChildUnaryPermissionArg::NotGranted, + write: ChildUnaryPermissionArg::NotGranted, + }) + } else { + Err(de::Error::invalid_value(de::Unexpected::Str(v), &self)) + } + } + + fn visit_map(self, mut v: V) -> Result + where + V: de::MapAccess<'de>, + { + let mut child_permissions_arg = ChildPermissionsArg::default(); + while let Some((key, value)) = + v.next_entry::()? + { + if key == "env" { + let arg = serde_json::from_value::(value); + child_permissions_arg.env = arg.map_err(|e| { + de::Error::custom(format!("(deno.permissions.env) {}", e)) + })?; + } else if key == "hrtime" { + let arg = serde_json::from_value::(value); + child_permissions_arg.hrtime = arg.map_err(|e| { + de::Error::custom(format!("(deno.permissions.hrtime) {}", e)) + })?; + } else if key == "net" { + let arg = serde_json::from_value::(value); + child_permissions_arg.net = arg.map_err(|e| { + de::Error::custom(format!("(deno.permissions.net) {}", e)) + })?; + } else if key == "ffi" { + let arg = serde_json::from_value::(value); + child_permissions_arg.ffi = arg.map_err(|e| { + de::Error::custom(format!("(deno.permissions.ffi) {}", e)) + })?; + } else if key == "read" { + let arg = serde_json::from_value::(value); + child_permissions_arg.read = arg.map_err(|e| { + de::Error::custom(format!("(deno.permissions.read) {}", e)) + })?; + } else if key == "run" { + let arg = serde_json::from_value::(value); + child_permissions_arg.run = arg.map_err(|e| { + de::Error::custom(format!("(deno.permissions.run) {}", e)) + })?; + } else if key == "write" { + let arg = serde_json::from_value::(value); + child_permissions_arg.write = arg.map_err(|e| { + de::Error::custom(format!("(deno.permissions.write) {}", e)) + })?; + } else { + return Err(de::Error::custom("unknown permission name")); + } + } + Ok(child_permissions_arg) + } + } + deserializer.deserialize_any(ChildPermissionsArgVisitor) + } +} + +pub fn create_child_permissions( + main_perms: &mut Permissions, + child_permissions_arg: ChildPermissionsArg, +) -> Result { + let mut worker_perms = Permissions::default(); + match child_permissions_arg.env { + ChildUnaryPermissionArg::Inherit => { + worker_perms.env = main_perms.env.clone(); + } + ChildUnaryPermissionArg::Granted => { + if main_perms.env.check_all().is_err() { + return Err(escalation_error()); + } + worker_perms.env.global_state = PermissionState::Granted; + } + ChildUnaryPermissionArg::NotGranted => {} + ChildUnaryPermissionArg::GrantedList(granted_list) => { + worker_perms.env.granted_list = + Permissions::new_env(&Some(granted_list), false).granted_list; + if !worker_perms + .env + .granted_list + .iter() + .all(|desc| main_perms.env.check(desc.as_ref()).is_ok()) + { + return Err(escalation_error()); + } + } + } + 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.prompt = main_perms.env.prompt; + match child_permissions_arg.hrtime { + ChildUnitPermissionArg::Inherit => { + worker_perms.hrtime = main_perms.hrtime.clone(); + } + ChildUnitPermissionArg::Granted => { + if main_perms.hrtime.check().is_err() { + return Err(escalation_error()); + } + worker_perms.hrtime.state = PermissionState::Granted; + } + ChildUnitPermissionArg::NotGranted => {} + } + if main_perms.hrtime.state == PermissionState::Denied { + worker_perms.hrtime.state = PermissionState::Denied; + } + worker_perms.hrtime.prompt = main_perms.hrtime.prompt; + match child_permissions_arg.net { + ChildUnaryPermissionArg::Inherit => { + worker_perms.net = main_perms.net.clone(); + } + ChildUnaryPermissionArg::Granted => { + if main_perms.net.check_all().is_err() { + return Err(escalation_error()); + } + worker_perms.net.global_state = PermissionState::Granted; + } + ChildUnaryPermissionArg::NotGranted => {} + ChildUnaryPermissionArg::GrantedList(granted_list) => { + worker_perms.net.granted_list = + Permissions::new_net(&Some(granted_list), false).granted_list; + if !worker_perms + .net + .granted_list + .iter() + .all(|desc| main_perms.net.check(&(&desc.0, desc.1)).is_ok()) + { + return Err(escalation_error()); + } + } + } + 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.prompt = main_perms.net.prompt; + match child_permissions_arg.ffi { + ChildUnaryPermissionArg::Inherit => { + worker_perms.ffi = main_perms.ffi.clone(); + } + ChildUnaryPermissionArg::Granted => { + if main_perms.ffi.check_all().is_err() { + return Err(escalation_error()); + } + worker_perms.ffi.global_state = PermissionState::Granted; + } + ChildUnaryPermissionArg::NotGranted => {} + ChildUnaryPermissionArg::GrantedList(granted_list) => { + worker_perms.ffi.granted_list = Permissions::new_ffi( + &Some(granted_list.iter().map(PathBuf::from).collect()), + false, + ) + .granted_list; + if !worker_perms + .ffi + .granted_list + .iter() + .all(|desc| main_perms.ffi.check(&desc.0).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.prompt = main_perms.ffi.prompt; + match child_permissions_arg.read { + ChildUnaryPermissionArg::Inherit => { + worker_perms.read = main_perms.read.clone(); + } + ChildUnaryPermissionArg::Granted => { + if main_perms.read.check_all().is_err() { + return Err(escalation_error()); + } + worker_perms.read.global_state = PermissionState::Granted; + } + ChildUnaryPermissionArg::NotGranted => {} + ChildUnaryPermissionArg::GrantedList(granted_list) => { + worker_perms.read.granted_list = Permissions::new_read( + &Some(granted_list.iter().map(PathBuf::from).collect()), + false, + ) + .granted_list; + if !worker_perms + .read + .granted_list + .iter() + .all(|desc| main_perms.read.check(&desc.0).is_ok()) + { + return Err(escalation_error()); + } + } + } + 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.prompt = main_perms.read.prompt; + match child_permissions_arg.run { + ChildUnaryPermissionArg::Inherit => { + worker_perms.run = main_perms.run.clone(); + } + ChildUnaryPermissionArg::Granted => { + if main_perms.run.check_all().is_err() { + return Err(escalation_error()); + } + worker_perms.run.global_state = PermissionState::Granted; + } + ChildUnaryPermissionArg::NotGranted => {} + ChildUnaryPermissionArg::GrantedList(granted_list) => { + worker_perms.run.granted_list = + Permissions::new_run(&Some(granted_list), false).granted_list; + if !worker_perms + .run + .granted_list + .iter() + .all(|desc| main_perms.run.check(&desc.to_string()).is_ok()) + { + return Err(escalation_error()); + } + } + } + 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.prompt = main_perms.run.prompt; + match child_permissions_arg.write { + ChildUnaryPermissionArg::Inherit => { + worker_perms.write = main_perms.write.clone(); + } + ChildUnaryPermissionArg::Granted => { + if main_perms.write.check_all().is_err() { + return Err(escalation_error()); + } + worker_perms.write.global_state = PermissionState::Granted; + } + ChildUnaryPermissionArg::NotGranted => {} + ChildUnaryPermissionArg::GrantedList(granted_list) => { + worker_perms.write.granted_list = Permissions::new_write( + &Some(granted_list.iter().map(PathBuf::from).collect()), + false, + ) + .granted_list; + if !worker_perms + .write + .granted_list + .iter() + .all(|desc| main_perms.write.check(&desc.0).is_ok()) + { + return Err(escalation_error()); + } + } + } + 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.prompt = main_perms.write.prompt; + Ok(worker_perms) +} + /// Shows the permission prompt and returns the answer according to the user input. /// This loops until the user gives the proper input. #[cfg(not(test))] @@ -1371,6 +2014,7 @@ fn set_prompt_result(value: bool) { mod tests { use super::*; use deno_core::resolve_url_or_path; + use deno_core::serde_json::json; // Creates vector of strings, Vec macro_rules! svec { @@ -1701,11 +2345,11 @@ mod tests { }, ffi: UnaryPermission { global_state: PermissionState::Prompt, - ..Permissions::new_ffi(&Some(svec!["deno"]), false) + ..Permissions::new_ffi(&Some(vec![PathBuf::from("deno")]), false) }, hrtime: UnitPermission { state: PermissionState::Prompt, - ..Default::default() + ..Permissions::new_hrtime(false, false) }, }; #[rustfmt::skip] @@ -1733,9 +2377,9 @@ mod tests { assert_eq!(perms2.run.query(None), PermissionState::Prompt); assert_eq!(perms2.run.query(Some(&"deno".to_string())), PermissionState::Granted); assert_eq!(perms1.ffi.query(None), PermissionState::Granted); - assert_eq!(perms1.ffi.query(Some(&"deno".to_string())), PermissionState::Granted); + assert_eq!(perms1.ffi.query(Some(Path::new("deno"))), PermissionState::Granted); assert_eq!(perms2.ffi.query(None), PermissionState::Prompt); - assert_eq!(perms2.ffi.query(Some(&"deno".to_string())), PermissionState::Granted); + assert_eq!(perms2.ffi.query(Some(Path::new("deno"))), PermissionState::Granted); assert_eq!(perms1.hrtime.query(), PermissionState::Granted); assert_eq!(perms2.hrtime.query(), PermissionState::Prompt); }; @@ -1772,10 +2416,10 @@ mod tests { set_prompt_result(false); assert_eq!(perms.run.request(Some(&"deno".to_string())), PermissionState::Granted); set_prompt_result(true); - assert_eq!(perms.ffi.request(Some(&"deno".to_string())), PermissionState::Granted); + assert_eq!(perms.ffi.request(Some(Path::new("deno"))), PermissionState::Granted); assert_eq!(perms.ffi.query(None), PermissionState::Prompt); set_prompt_result(false); - assert_eq!(perms.ffi.request(Some(&"deno".to_string())), PermissionState::Granted); + assert_eq!(perms.ffi.request(Some(Path::new("deno"))), PermissionState::Granted); set_prompt_result(false); assert_eq!(perms.hrtime.request(), PermissionState::Denied); set_prompt_result(true); @@ -1817,11 +2461,11 @@ mod tests { }, ffi: UnaryPermission { global_state: PermissionState::Prompt, - ..Permissions::new_ffi(&Some(svec!["deno"]), false) + ..Permissions::new_ffi(&Some(vec![PathBuf::from("deno")]), false) }, hrtime: UnitPermission { state: PermissionState::Denied, - ..Default::default() + ..Permissions::new_hrtime(false, false) }, }; #[rustfmt::skip] @@ -1837,7 +2481,7 @@ mod tests { assert_eq!(perms.net.query(Some(&("127.0.0.1", Some(8000)))), PermissionState::Granted); assert_eq!(perms.env.revoke(Some(&"HOME".to_string())), PermissionState::Prompt); assert_eq!(perms.run.revoke(Some(&"deno".to_string())), PermissionState::Prompt); - assert_eq!(perms.ffi.revoke(Some(&"deno".to_string())), PermissionState::Prompt); + assert_eq!(perms.ffi.revoke(Some(Path::new("deno"))), PermissionState::Prompt); assert_eq!(perms.hrtime.revoke(), PermissionState::Denied); }; } @@ -1977,4 +2621,233 @@ mod tests { PermissionState::Prompt ); } + + #[test] + fn test_deserialize_child_permissions_arg() { + assert_eq!( + ChildPermissionsArg::default(), + ChildPermissionsArg { + env: ChildUnaryPermissionArg::Inherit, + hrtime: ChildUnitPermissionArg::Inherit, + net: ChildUnaryPermissionArg::Inherit, + ffi: ChildUnaryPermissionArg::Inherit, + read: ChildUnaryPermissionArg::Inherit, + run: ChildUnaryPermissionArg::Inherit, + write: ChildUnaryPermissionArg::Inherit, + } + ); + assert_eq!( + serde_json::from_value::(json!("inherit")).unwrap(), + ChildPermissionsArg::default() + ); + assert_eq!( + serde_json::from_value::(json!("none")).unwrap(), + ChildPermissionsArg { + env: ChildUnaryPermissionArg::NotGranted, + hrtime: ChildUnitPermissionArg::NotGranted, + net: ChildUnaryPermissionArg::NotGranted, + ffi: ChildUnaryPermissionArg::NotGranted, + read: ChildUnaryPermissionArg::NotGranted, + run: ChildUnaryPermissionArg::NotGranted, + write: ChildUnaryPermissionArg::NotGranted, + } + ); + assert_eq!( + serde_json::from_value::(json!({})).unwrap(), + ChildPermissionsArg::default() + ); + assert_eq!( + serde_json::from_value::(json!({ + "env": ["foo", "bar"], + })) + .unwrap(), + ChildPermissionsArg { + env: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar"]), + ..Default::default() + } + ); + assert_eq!( + serde_json::from_value::(json!({ + "hrtime": true, + })) + .unwrap(), + ChildPermissionsArg { + hrtime: ChildUnitPermissionArg::Granted, + ..Default::default() + } + ); + assert_eq!( + serde_json::from_value::(json!({ + "hrtime": false, + })) + .unwrap(), + ChildPermissionsArg { + hrtime: ChildUnitPermissionArg::NotGranted, + ..Default::default() + } + ); + assert_eq!( + serde_json::from_value::(json!({ + "env": true, + "net": true, + "ffi": true, + "read": true, + "run": true, + "write": true, + })) + .unwrap(), + ChildPermissionsArg { + env: ChildUnaryPermissionArg::Granted, + net: ChildUnaryPermissionArg::Granted, + ffi: ChildUnaryPermissionArg::Granted, + read: ChildUnaryPermissionArg::Granted, + run: ChildUnaryPermissionArg::Granted, + write: ChildUnaryPermissionArg::Granted, + ..Default::default() + } + ); + assert_eq!( + serde_json::from_value::(json!({ + "env": false, + "net": false, + "ffi": false, + "read": false, + "run": false, + "write": false, + })) + .unwrap(), + ChildPermissionsArg { + env: ChildUnaryPermissionArg::NotGranted, + net: ChildUnaryPermissionArg::NotGranted, + ffi: ChildUnaryPermissionArg::NotGranted, + read: ChildUnaryPermissionArg::NotGranted, + run: ChildUnaryPermissionArg::NotGranted, + write: ChildUnaryPermissionArg::NotGranted, + ..Default::default() + } + ); + assert_eq!( + serde_json::from_value::(json!({ + "env": ["foo", "bar"], + "net": ["foo", "bar:8000"], + "ffi": ["foo", "file:///bar/baz"], + "read": ["foo", "file:///bar/baz"], + "run": ["foo", "file:///bar/baz", "./qux"], + "write": ["foo", "file:///bar/baz"], + })) + .unwrap(), + ChildPermissionsArg { + env: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar"]), + net: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar:8000"]), + ffi: ChildUnaryPermissionArg::GrantedList(svec![ + "foo", + "file:///bar/baz" + ]), + read: ChildUnaryPermissionArg::GrantedList(svec![ + "foo", + "file:///bar/baz" + ]), + run: ChildUnaryPermissionArg::GrantedList(svec![ + "foo", + "file:///bar/baz", + "./qux" + ]), + write: ChildUnaryPermissionArg::GrantedList(svec![ + "foo", + "file:///bar/baz" + ]), + ..Default::default() + } + ); + } + + #[test] + fn test_create_child_permissions() { + let mut main_perms = Permissions { + env: Permissions::new_env(&Some(vec![]), false), + hrtime: Permissions::new_hrtime(true, false), + net: Permissions::new_net(&Some(svec!["foo", "bar"]), false), + ..Default::default() + }; + assert_eq!( + create_child_permissions( + &mut main_perms.clone(), + ChildPermissionsArg { + env: ChildUnaryPermissionArg::Inherit, + hrtime: ChildUnitPermissionArg::NotGranted, + net: ChildUnaryPermissionArg::GrantedList(svec!["foo"]), + ffi: ChildUnaryPermissionArg::NotGranted, + ..Default::default() + } + ) + .unwrap(), + Permissions { + env: Permissions::new_env(&Some(vec![]), false), + net: Permissions::new_net(&Some(svec!["foo"]), false), + ..Default::default() + } + ); + assert!(create_child_permissions( + &mut main_perms.clone(), + ChildPermissionsArg { + net: ChildUnaryPermissionArg::Granted, + ..Default::default() + } + ) + .is_err()); + assert!(create_child_permissions( + &mut main_perms.clone(), + ChildPermissionsArg { + net: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar", "baz"]), + ..Default::default() + } + ) + .is_err()); + assert!(create_child_permissions( + &mut main_perms, + ChildPermissionsArg { + ffi: ChildUnaryPermissionArg::GrantedList(svec!["foo"]), + ..Default::default() + } + ) + .is_err()); + } + + #[test] + fn test_create_child_permissions_with_prompt() { + let _guard = PERMISSION_PROMPT_GUARD.lock(); + let mut main_perms = Permissions::from_options(&PermissionsOptions { + prompt: true, + ..Default::default() + }); + set_prompt_result(true); + let worker_perms = create_child_permissions( + &mut main_perms, + ChildPermissionsArg { + hrtime: ChildUnitPermissionArg::Granted, + read: ChildUnaryPermissionArg::Granted, + run: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar"]), + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(main_perms, worker_perms); + } + + #[test] + fn test_create_child_permissions_with_inherited_denied_list() { + let _guard = PERMISSION_PROMPT_GUARD.lock(); + let mut main_perms = Permissions::from_options(&PermissionsOptions { + prompt: true, + ..Default::default() + }); + set_prompt_result(false); + assert!(main_perms.write.check(&PathBuf::from("foo")).is_err()); + let worker_perms = create_child_permissions( + &mut main_perms.clone(), + ChildPermissionsArg::default(), + ) + .unwrap(); + assert_eq!(worker_perms.write.denied_list, main_perms.write.denied_list); + } }