diff options
| author | Steven Guerrero <stephenguerrero43@gmail.com> | 2021-01-06 15:31:16 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-01-06 21:31:16 +0100 |
| commit | adc2f08c178f51b3ddd5f1c2e3d7f5603424521e (patch) | |
| tree | 99d61fca9bfdad4e427f9d2d6d1719f69d09c96a | |
| parent | 2e18fcebcc2ee931ee952ac2fe2175d6ec7acf69 (diff) | |
feat: Add configurable permissions for Workers (#8215)
This commit adds new option to "Worker" Web API that allows to
configure permissions.
New "Worker.deno.permissions" option can be used to define limited
permissions to the worker thread by either:
- inherit set of parent thread permissions
- use limited subset of parent thread permissions
- revoke all permissions (full sandbox)
In order to achieve this functionality "CliModuleLoader"
was modified to accept "initial permissions", which are used
for top module loading (ie. uses parent thread permission set
to load top level module of a worker).
| -rw-r--r-- | cli/dts/lib.deno.shared_globals.d.ts | 56 | ||||
| -rw-r--r-- | cli/main.rs | 5 | ||||
| -rw-r--r-- | cli/module_loader.rs | 22 | ||||
| -rw-r--r-- | cli/tests/unstable_worker.ts | 6 | ||||
| -rw-r--r-- | cli/tests/workers/bench_worker.ts (renamed from cli/tests/subdir/bench_worker.ts) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/busy_worker.js (renamed from cli/tests/subdir/busy_worker.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/deno_worker.ts (renamed from cli/tests/subdir/deno_worker.ts) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/event_worker.js (renamed from cli/tests/subdir/event_worker.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/event_worker_scope.js (renamed from cli/tests/subdir/event_worker_scope.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/fetching_worker.js (renamed from cli/tests/subdir/fetching_worker.js) | 2 | ||||
| -rw-r--r-- | cli/tests/workers/immediately_close_worker.js (renamed from cli/tests/immediately_close_worker.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/nested_worker.js (renamed from cli/tests/subdir/nested_worker.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/no_permissions_worker.js | 17 | ||||
| -rw-r--r-- | cli/tests/workers/non_deno_worker.js (renamed from cli/tests/subdir/non_deno_worker.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/parent_read_check_granular_worker.js | 43 | ||||
| -rw-r--r-- | cli/tests/workers/parent_read_check_worker.js | 27 | ||||
| -rw-r--r-- | cli/tests/workers/racy_worker.js (renamed from cli/tests/subdir/racy_worker.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/read_check_granular_worker.js | 13 | ||||
| -rw-r--r-- | cli/tests/workers/read_check_worker.js | 7 | ||||
| -rw-r--r-- | cli/tests/workers/sibling_worker.js (renamed from cli/tests/subdir/sibling_worker.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/test_worker.js (renamed from cli/tests/subdir/test_worker.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/test_worker.ts (renamed from cli/tests/subdir/test_worker.ts) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/throwing_worker.js (renamed from cli/tests/subdir/throwing_worker.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/worker_crypto.js (renamed from cli/tests/subdir/worker_crypto.js) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/worker_globals.ts (renamed from cli/tests/subdir/worker_globals.ts) | 0 | ||||
| -rw-r--r-- | cli/tests/workers/worker_unstable.ts (renamed from cli/tests/subdir/worker_unstable.ts) | 0 | ||||
| -rw-r--r-- | cli/tests/workers_round_robin_bench.ts | 2 | ||||
| -rw-r--r-- | cli/tests/workers_startup_bench.ts | 2 | ||||
| -rw-r--r-- | cli/tests/workers_test.ts | 280 | ||||
| -rw-r--r-- | docs/runtime/workers.md | 147 | ||||
| -rw-r--r-- | runtime/js/11_workers.js | 128 | ||||
| -rw-r--r-- | runtime/ops/worker_host.rs | 376 | ||||
| -rw-r--r-- | runtime/permissions.rs | 2 |
33 files changed, 1062 insertions, 73 deletions
diff --git a/cli/dts/lib.deno.shared_globals.d.ts b/cli/dts/lib.deno.shared_globals.d.ts index e4d763ec0..74abbc95b 100644 --- a/cli/dts/lib.deno.shared_globals.d.ts +++ b/cli/dts/lib.deno.shared_globals.d.ts @@ -662,24 +662,33 @@ declare class Worker extends EventTarget { options?: { type?: "classic" | "module"; name?: string; - /** UNSTABLE: New API. Expect many changes; most likely this - * field will be made into an object for more granular - * configuration of worker thread (permissions, import map, etc.). + /** UNSTABLE: New API. * - * Set to `true` to make `Deno` namespace and all of its methods - * available to worker thread. - * - * Currently worker inherits permissions from main thread (permissions - * given using `--allow-*` flags). - * Configurable permissions are on the roadmap to be implemented. + * Set deno.namespace to `true` to make `Deno` namespace and all of its methods + * available to worker thread. The namespace is disabled by default. + * + * Configure deno.permissions options to change the level of access the worker will + * have. By default it will inherit the permissions of its parent thread. The permissions + * of a worker can't be extended beyond its parent's permissions reach. + * - "inherit" will take the permissions of the thread the worker is created in + * - You can disable/enable permissions all together by passing a boolean + * - You can provide a list of routes relative to the file the worker + * is created in to limit the access of the worker (read/write permissions only) * * Example: * * ```ts * // mod.ts * const worker = new Worker( - * new URL("deno_worker.ts", import.meta.url).href, - * { type: "module", deno: true } + * new URL("deno_worker.ts", import.meta.url).href, { + * type: "module", + * deno: { + * namespace: true, + * permissions: { + * read: true, + * }, + * }, + * } * ); * worker.postMessage({ cmd: "readFile", fileName: "./log.txt" }); * @@ -707,7 +716,30 @@ declare class Worker extends EventTarget { * hello world2 * */ - deno?: boolean; + // TODO(Soremwar) + // `deno: true` is kept for backwards compatibility with the previous worker + // options implementation. Remove for 2.0 + deno?: true | { + namespace?: boolean; + /** Set to false to disable all the permissions in the worker */ + permissions?: "inherit" | false | { + env?: "inherit" | boolean; + hrtime?: "inherit" | boolean; + /** + * The format of the net access list must be `hostname[:port]` + * in order to be resolved + * + * ``` + * net: ["https://deno.land", "localhost:8080"], + * ``` + * */ + net?: "inherit" | boolean | string[]; + plugin?: "inherit" | boolean; + read?: "inherit" | boolean | Array<string | URL>; + run?: "inherit" | boolean; + write?: "inherit" | boolean | Array<string | URL>; + }; + }; }, ); postMessage(message: any, transfer: ArrayBuffer[]): void; diff --git a/cli/main.rs b/cli/main.rs index b622cccf3..a247389d9 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -100,7 +100,10 @@ fn create_web_worker_callback( || program_state.coverage_dir.is_some(); let maybe_inspector_server = program_state.maybe_inspector_server.clone(); - let module_loader = CliModuleLoader::new_for_worker(program_state.clone()); + let module_loader = CliModuleLoader::new_for_worker( + program_state.clone(), + args.parent_permissions.clone(), + ); let create_web_worker_cb = create_web_worker_callback(program_state.clone()); diff --git a/cli/module_loader.rs b/cli/module_loader.rs index aab951c4a..c91a2336b 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -22,6 +22,10 @@ pub struct CliModuleLoader { /// import map file will be resolved and set. pub import_map: Option<ImportMap>, pub lib: TypeLib, + /// The initial set of permissions used to resolve the imports in the worker. + /// They are decoupled from the worker permissions since read access errors + /// must be raised based on the parent thread permissions + pub initial_permissions: Rc<RefCell<Option<Permissions>>>, pub program_state: Arc<ProgramState>, } @@ -38,11 +42,15 @@ impl CliModuleLoader { Rc::new(CliModuleLoader { import_map, lib, + initial_permissions: Rc::new(RefCell::new(None)), program_state, }) } - pub fn new_for_worker(program_state: Arc<ProgramState>) -> Rc<Self> { + pub fn new_for_worker( + program_state: Arc<ProgramState>, + permissions: Permissions, + ) -> Rc<Self> { let lib = if program_state.flags.unstable { TypeLib::UnstableDenoWorker } else { @@ -52,6 +60,7 @@ impl CliModuleLoader { Rc::new(CliModuleLoader { import_map: None, lib, + initial_permissions: Rc::new(RefCell::new(Some(permissions))), program_state, }) } @@ -118,7 +127,16 @@ impl ModuleLoader for CliModuleLoader { let state = op_state.borrow(); // The permissions that should be applied to any dynamically imported module - let dynamic_permissions = state.borrow::<Permissions>().clone(); + let dynamic_permissions = + // If there are initial permissions assigned to the loader take them + // and use only once for top level module load. + // Otherwise use permissions assigned to the current worker. + if let Some(permissions) = self.initial_permissions.borrow_mut().take() { + permissions + } else { + state.borrow::<Permissions>().clone() + }; + let lib = self.lib.clone(); drop(state); diff --git a/cli/tests/unstable_worker.ts b/cli/tests/unstable_worker.ts index 6b5304edf..429754dfe 100644 --- a/cli/tests/unstable_worker.ts +++ b/cli/tests/unstable_worker.ts @@ -1,8 +1,10 @@ const w = new Worker( - new URL("subdir/worker_unstable.ts", import.meta.url).href, + new URL("workers/worker_unstable.ts", import.meta.url).href, { type: "module", - deno: true, + deno: { + namespace: true, + }, name: "Unstable Worker", }, ); diff --git a/cli/tests/subdir/bench_worker.ts b/cli/tests/workers/bench_worker.ts index 7e85eed03..7e85eed03 100644 --- a/cli/tests/subdir/bench_worker.ts +++ b/cli/tests/workers/bench_worker.ts diff --git a/cli/tests/subdir/busy_worker.js b/cli/tests/workers/busy_worker.js index 7deba0321..7deba0321 100644 --- a/cli/tests/subdir/busy_worker.js +++ b/cli/tests/workers/busy_worker.js diff --git a/cli/tests/subdir/deno_worker.ts b/cli/tests/workers/deno_worker.ts index 6a57c47f0..6a57c47f0 100644 --- a/cli/tests/subdir/deno_worker.ts +++ b/cli/tests/workers/deno_worker.ts diff --git a/cli/tests/subdir/event_worker.js b/cli/tests/workers/event_worker.js index 849b6026c..849b6026c 100644 --- a/cli/tests/subdir/event_worker.js +++ b/cli/tests/workers/event_worker.js diff --git a/cli/tests/subdir/event_worker_scope.js b/cli/tests/workers/event_worker_scope.js index 0381801a8..0381801a8 100644 --- a/cli/tests/subdir/event_worker_scope.js +++ b/cli/tests/workers/event_worker_scope.js diff --git a/cli/tests/subdir/fetching_worker.js b/cli/tests/workers/fetching_worker.js index 3e33d1c9e..e1bcdf911 100644 --- a/cli/tests/subdir/fetching_worker.js +++ b/cli/tests/workers/fetching_worker.js @@ -1,5 +1,5 @@ const r = await fetch( - "http://localhost:4545/cli/tests/subdir/fetching_worker.js", + "http://localhost:4545/cli/tests/workers/fetching_worker.js", ); await r.text(); postMessage("Done!"); diff --git a/cli/tests/immediately_close_worker.js b/cli/tests/workers/immediately_close_worker.js index 8fd27343a..8fd27343a 100644 --- a/cli/tests/immediately_close_worker.js +++ b/cli/tests/workers/immediately_close_worker.js diff --git a/cli/tests/subdir/nested_worker.js b/cli/tests/workers/nested_worker.js index 4b51b8763..4b51b8763 100644 --- a/cli/tests/subdir/nested_worker.js +++ b/cli/tests/workers/nested_worker.js diff --git a/cli/tests/workers/no_permissions_worker.js b/cli/tests/workers/no_permissions_worker.js new file mode 100644 index 000000000..8a4f79d57 --- /dev/null +++ b/cli/tests/workers/no_permissions_worker.js @@ -0,0 +1,17 @@ +self.onmessage = async () => { + const hrtime = await Deno.permissions.query({ name: "hrtime" }); + const net = await Deno.permissions.query({ name: "net" }); + const plugin = await Deno.permissions.query({ name: "plugin" }); + const read = await Deno.permissions.query({ name: "read" }); + const run = await Deno.permissions.query({ name: "run" }); + const write = await Deno.permissions.query({ name: "write" }); + self.postMessage( + hrtime.state === "denied" && + net.state === "denied" && + plugin.state === "denied" && + read.state === "denied" && + run.state === "denied" && + write.state === "denied", + ); + self.close(); +}; diff --git a/cli/tests/subdir/non_deno_worker.js b/cli/tests/workers/non_deno_worker.js index 773721560..773721560 100644 --- a/cli/tests/subdir/non_deno_worker.js +++ b/cli/tests/workers/non_deno_worker.js diff --git a/cli/tests/workers/parent_read_check_granular_worker.js b/cli/tests/workers/parent_read_check_granular_worker.js new file mode 100644 index 000000000..1a7182e17 --- /dev/null +++ b/cli/tests/workers/parent_read_check_granular_worker.js @@ -0,0 +1,43 @@ +import { fromFileUrl } from "../../../std/path/mod.ts"; + +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: fromFileUrl(new URL(data.route, import.meta.url)), + }); + + messages[data.index] = state === "granted"; + + worker.postMessage({ + index: data.index, + route: data.route, + }); +}; diff --git a/cli/tests/workers/parent_read_check_worker.js b/cli/tests/workers/parent_read_check_worker.js new file mode 100644 index 000000000..ec92cca3f --- /dev/null +++ b/cli/tests/workers/parent_read_check_worker.js @@ -0,0 +1,27 @@ +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, + }, + }, + }, + ); + + worker.onmessage = ({ data: childHasPermission }) => { + postMessage({ + parentHasPermission: state === "granted", + childHasPermission, + }); + close(); + }; + worker.postMessage(null); +}; diff --git a/cli/tests/subdir/racy_worker.js b/cli/tests/workers/racy_worker.js index 83756b791..83756b791 100644 --- a/cli/tests/subdir/racy_worker.js +++ b/cli/tests/workers/racy_worker.js diff --git a/cli/tests/workers/read_check_granular_worker.js b/cli/tests/workers/read_check_granular_worker.js new file mode 100644 index 000000000..4eddb7a75 --- /dev/null +++ b/cli/tests/workers/read_check_granular_worker.js @@ -0,0 +1,13 @@ +import { fromFileUrl } from "../../../std/path/mod.ts"; + +onmessage = async ({ data }) => { + const { state } = await Deno.permissions.query({ + name: "read", + path: fromFileUrl(new URL(data.route, import.meta.url)), + }); + + postMessage({ + hasPermission: state === "granted", + index: data.index, + }); +}; diff --git a/cli/tests/workers/read_check_worker.js b/cli/tests/workers/read_check_worker.js new file mode 100644 index 000000000..2ad01bf5b --- /dev/null +++ b/cli/tests/workers/read_check_worker.js @@ -0,0 +1,7 @@ +onmessage = async () => { + const { state } = await Deno.permissions.query({ + name: "read", + }); + postMessage(state === "granted"); + close(); +}; diff --git a/cli/tests/subdir/sibling_worker.js b/cli/tests/workers/sibling_worker.js index 99707e5d6..99707e5d6 100644 --- a/cli/tests/subdir/sibling_worker.js +++ b/cli/tests/workers/sibling_worker.js diff --git a/cli/tests/subdir/test_worker.js b/cli/tests/workers/test_worker.js index 4260975a6..4260975a6 100644 --- a/cli/tests/subdir/test_worker.js +++ b/cli/tests/workers/test_worker.js diff --git a/cli/tests/subdir/test_worker.ts b/cli/tests/workers/test_worker.ts index ca79dcfe4..ca79dcfe4 100644 --- a/cli/tests/subdir/test_worker.ts +++ b/cli/tests/workers/test_worker.ts diff --git a/cli/tests/subdir/throwing_worker.js b/cli/tests/workers/throwing_worker.js index 56ee4ff88..56ee4ff88 100644 --- a/cli/tests/subdir/throwing_worker.js +++ b/cli/tests/workers/throwing_worker.js diff --git a/cli/tests/subdir/worker_crypto.js b/cli/tests/workers/worker_crypto.js index a86340005..a86340005 100644 --- a/cli/tests/subdir/worker_crypto.js +++ b/cli/tests/workers/worker_crypto.js diff --git a/cli/tests/subdir/worker_globals.ts b/cli/tests/workers/worker_globals.ts index a9e7efd85..a9e7efd85 100644 --- a/cli/tests/subdir/worker_globals.ts +++ b/cli/tests/workers/worker_globals.ts diff --git a/cli/tests/subdir/worker_unstable.ts b/cli/tests/workers/worker_unstable.ts index a5b5f7ba2..a5b5f7ba2 100644 --- a/cli/tests/subdir/worker_unstable.ts +++ b/cli/tests/workers/worker_unstable.ts diff --git a/cli/tests/workers_round_robin_bench.ts b/cli/tests/workers_round_robin_bench.ts index 8467480b8..461e86f91 100644 --- a/cli/tests/workers_round_robin_bench.ts +++ b/cli/tests/workers_round_robin_bench.ts @@ -22,7 +22,7 @@ async function main(): Promise<void> { const workers: Array<[Map<number, Deferred<string>>, Worker]> = []; for (let i = 1; i <= workerCount; ++i) { const worker = new Worker( - new URL("subdir/bench_worker.ts", import.meta.url).href, + new URL("workers/bench_worker.ts", import.meta.url).href, { type: "module" }, ); const promise = deferred(); diff --git a/cli/tests/workers_startup_bench.ts b/cli/tests/workers_startup_bench.ts index 0d58b7912..f85bd13a1 100644 --- a/cli/tests/workers_startup_bench.ts +++ b/cli/tests/workers_startup_bench.ts @@ -5,7 +5,7 @@ async function bench(): Promise<void> { const workers: Worker[] = []; for (let i = 1; i <= workerCount; ++i) { const worker = new Worker( - new URL("subdir/bench_worker.ts", import.meta.url).href, + new URL("workers/bench_worker.ts", import.meta.url).href, { type: "module" }, ); const promise = new Promise<void>((resolve): void => { diff --git a/cli/tests/workers_test.ts b/cli/tests/workers_test.ts index 3bfe0181a..2dfc7e26b 100644 --- a/cli/tests/workers_test.ts +++ b/cli/tests/workers_test.ts @@ -2,12 +2,12 @@ // Requires to be run with `--allow-net` flag -// FIXME(bartlomieju): this file is an integration test only because -// workers are leaking ops at the moment - `worker.terminate()` is not -// yet implemented. Once it gets implemented this file should be -// again moved to `cli/js/` as an unit test file. - -import { assert, assertEquals } from "../../std/testing/asserts.ts"; +import { + assert, + assertEquals, + assertThrows, + fail, +} from "../../std/testing/asserts.ts"; import { deferred } from "../../std/async/deferred.ts"; Deno.test({ @@ -16,11 +16,11 @@ Deno.test({ const promise = deferred(); const jsWorker = new Worker( - new URL("subdir/test_worker.js", import.meta.url).href, + new URL("workers/test_worker.js", import.meta.url).href, { type: "module" }, ); const tsWorker = new Worker( - new URL("subdir/test_worker.ts", import.meta.url).href, + new URL("workers/test_worker.ts", import.meta.url).href, { type: "module", name: "tsWorker" }, ); @@ -73,7 +73,7 @@ Deno.test({ const promise = deferred(); const nestedWorker = new Worker( - new URL("subdir/nested_worker.js", import.meta.url).href, + new URL("workers/nested_worker.js", import.meta.url).href, { type: "module", name: "nested" }, ); @@ -93,7 +93,7 @@ Deno.test({ fn: async function (): Promise<void> { const promise = deferred(); const throwingWorker = new Worker( - new URL("subdir/throwing_worker.js", import.meta.url).href, + new URL("workers/throwing_worker.js", import.meta.url).href, { type: "module" }, ); @@ -114,7 +114,7 @@ Deno.test({ fn: async function (): Promise<void> { const promise = deferred(); const w = new Worker( - new URL("subdir/worker_globals.ts", import.meta.url).href, + new URL("workers/worker_globals.ts", import.meta.url).href, { type: "module" }, ); w.onmessage = (e): void => { @@ -133,7 +133,7 @@ Deno.test({ const promise = deferred(); const fetchingWorker = new Worker( - new URL("subdir/fetching_worker.js", import.meta.url).href, + new URL("workers/fetching_worker.js", import.meta.url).href, { type: "module" }, ); @@ -160,7 +160,7 @@ Deno.test({ const promise = deferred(); const busyWorker = new Worker( - new URL("subdir/busy_worker.js", import.meta.url).href, + new URL("workers/busy_worker.js", import.meta.url).href, { type: "module" }, ); @@ -193,7 +193,7 @@ Deno.test({ const promise = deferred(); const racyWorker = new Worker( - new URL("subdir/racy_worker.js", import.meta.url).href, + new URL("workers/racy_worker.js", import.meta.url).href, { type: "module" }, ); @@ -221,7 +221,7 @@ Deno.test({ const promise2 = deferred(); const worker = new Worker( - new URL("subdir/event_worker.js", import.meta.url).href, + new URL("workers/event_worker.js", import.meta.url).href, { type: "module" }, ); @@ -265,7 +265,7 @@ Deno.test({ const promise1 = deferred(); const worker = new Worker( - new URL("subdir/event_worker_scope.js", import.meta.url).href, + new URL("workers/event_worker_scope.js", import.meta.url).href, { type: "module" }, ); @@ -294,12 +294,18 @@ Deno.test({ const promise2 = deferred(); const regularWorker = new Worker( - new URL("subdir/non_deno_worker.js", import.meta.url).href, + new URL("workers/non_deno_worker.js", import.meta.url).href, { type: "module" }, ); const denoWorker = new Worker( - new URL("subdir/deno_worker.ts", import.meta.url).href, - { type: "module", deno: true }, + new URL("workers/deno_worker.ts", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: "inherit", + }, + }, ); regularWorker.onmessage = (e): void => { @@ -326,7 +332,7 @@ Deno.test({ fn: async function (): Promise<void> { const promise = deferred(); const w = new Worker( - new URL("subdir/worker_crypto.js", import.meta.url).href, + new URL("workers/worker_crypto.js", import.meta.url).href, { type: "module" }, ); w.onmessage = (e): void => { @@ -344,7 +350,7 @@ Deno.test({ fn: async function (): Promise<void> { const promise = deferred(); const w = new Worker( - new URL("subdir/test_worker.ts", import.meta.url).href, + new URL("workers/test_worker.ts", import.meta.url).href, { type: "module", name: "tsWorker" }, ); const arr: number[] = []; @@ -368,7 +374,7 @@ Deno.test({ fn: async function (): Promise<void> { const promise = deferred(); const w = new Worker( - new URL("./immediately_close_worker.js", import.meta.url).href, + new URL("./workers/immediately_close_worker.js", import.meta.url).href, { type: "module" }, ); setTimeout(() => { @@ -378,3 +384,233 @@ Deno.test({ w.terminate(); }, }); + +Deno.test("Worker inherits permissions", async function () { + const promise = deferred(); + const worker = new Worker( + new URL("./workers/read_check_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: "inherit", + }, + }, + ); + + worker.onmessage = ({ data: hasPermission }) => { + assert(hasPermission); + promise.resolve(); + }; + + worker.postMessage(null); + + await promise; + worker.terminate(); +}); + +Deno.test("Worker limit children permissions", async function () { + const promise = deferred(); + const worker = new Worker( + new URL("./workers/read_check_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: { + read: false, + }, + }, + }, + ); + + worker.onmessage = ({ data: hasPermission }) => { + assert(!hasPermission); + promise.resolve(); + }; + + worker.postMessage(null); + + await promise; + worker.terminate(); +}); + +Deno.test("Worker limit children permissions granularly", async function () { + const promise = deferred(); + const worker = new Worker( + new URL("./workers/read_check_granular_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: { + read: [ + new URL("./workers/read_check_worker.js", import.meta.url), + ], + }, + }, + }, + ); + + //Routes are relative to the spawned worker location + const routes = [ + { permission: false, route: "read_check_granular_worker.js" }, + { permission: true, route: "read_check_worker.js" }, + ]; + + let checked = 0; + worker.onmessage = ({ data }) => { + checked++; + assertEquals(data.hasPermission, routes[data.index].permission); + routes.shift(); + if (checked === routes.length) { + promise.resolve(); + } + }; + + routes.forEach(({ route }, index) => + worker.postMessage({ + index, + route, + }) + ); + + await promise; + 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 */ + const worker = new Worker( + new URL("./workers/parent_read_check_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: "inherit", + }, + }, + ); + + 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("./workers/parent_read_check_granular_worker.js", import.meta.url) + .href, + { + type: "module", + deno: { + namespace: true, + permissions: { + read: [ + new URL("./workers/read_check_granular_worker.js", import.meta.url), + ], + }, + }, + }, + ); + + //Routes are relative to the spawned worker location + const routes = [ + { + childHasPermission: false, + parentHasPermission: true, + route: "read_check_granular_worker.js", + }, + { + childHasPermission: false, + parentHasPermission: false, + route: "read_check_worker.js", + }, + ]; + + 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(({ route }, index) => + worker.postMessage({ + index, + route, + }) + ); + + await promise; + 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("./workers/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", + ); +}); + +Deno.test("Worker with disabled permissions", async function () { + const promise = deferred(); + + const worker = new Worker( + new URL("./workers/no_permissions_worker.js", import.meta.url).href, + { + type: "module", + deno: { + namespace: true, + permissions: false, + }, + }, + ); + + worker.onmessage = ({ data: sandboxed }) => { + assert(sandboxed); + promise.resolve(); + }; + + worker.postMessage(null); + await promise; + worker.terminate(); +}); diff --git a/docs/runtime/workers.md b/docs/runtime/workers.md index 5dcf05915..caed638b0 100644 --- a/docs/runtime/workers.md +++ b/docs/runtime/workers.md @@ -16,15 +16,15 @@ specifier for some nearby script. ```ts // Good -new Worker(new URL("worker.js", import.meta.url).href, { type: "module" }); +new Worker(new URL("./worker.js", import.meta.url).href, { type: "module" }); // Bad -new Worker(new URL("worker.js", import.meta.url).href); -new Worker(new URL("worker.js", import.meta.url).href, { type: "classic" }); +new Worker(new URL("./worker.js", import.meta.url).href); +new Worker(new URL("./worker.js", import.meta.url).href, { type: "classic" }); new Worker("./worker.js", { type: "module" }); ``` -### Permissions +### Instantiation permissions Creating a new `Worker` instance is similar to a dynamic import; therefore Deno requires appropriate permission for this action. @@ -34,7 +34,7 @@ For workers using local modules; `--allow-read` permission is required: **main.ts** ```ts -new Worker(new URL("worker.ts", import.meta.url).href, { type: "module" }); +new Worker(new URL("./worker.ts", import.meta.url).href, { type: "module" }); ``` **worker.ts** @@ -82,14 +82,17 @@ hello world By default the `Deno` namespace is not available in worker scope. -To add the `Deno` namespace pass `deno: true` option when creating new worker: +To enable the `Deno` namespace pass `deno.namespace = true` option when creating +new worker: **main.js** ```ts -const worker = new Worker(new URL("worker.js", import.meta.url).href, { +const worker = new Worker(new URL("./worker.js", import.meta.url).href, { type: "module", - deno: true, + deno: { + namespace: true, + }, }); worker.postMessage({ filename: "./log.txt" }); ``` @@ -116,7 +119,129 @@ $ deno run --allow-read --unstable main.js hello world ``` -When the `Deno` namespace is available in worker scope, the worker inherits its -parent process' permissions (the ones specified using `--allow-*` flags). +### Specifying worker permissions -We intend to make permissions configurable for workers. +> This is an unstable Deno feature. Learn more about +> [unstable features](./stability.md). + +The permissions available for the worker are analogous to the CLI permission +flags, meaning every permission enabled there can be disabled at the level of +the Worker API. You can find a more detailed description of each of the +permission options [here](../getting_started/permissions.md). + +By default a worker will inherit permissions from the thread it was created in, +however in order to allow users to limit the access of this worker we provide +the `deno.permissions` option in the worker API. + +- For permissions that support granular access you can pass in a list of the + desired resources the worker will have access to, and for those who only have + the on/off option you can pass true/false respectively. + + ```ts + const worker = new Worker(new URL("./worker.js", import.meta.url).href, { + type: "module", + deno: { + namespace: true, + permissions: [ + net: [ + "https://deno.land/", + ], + read: [ + new URL("./file_1.txt", import.meta.url), + new URL("./file_2.txt", import.meta.url), + ], + write: false, + ], + }, + }); + ``` + +- Granular access permissions receive both absolute and relative routes as + arguments, however take into account that relative routes will be resolved + relative to the file the worker is instantiated in, not the path the worker + file is currently in + + ```ts + const worker = new Worker(new URL("./worker/worker.js", import.meta.url).href, { + type: "module", + deno: { + namespace: true, + permissions: [ + read: [ + "/home/user/Documents/deno/worker/file_1.txt", + "./worker/file_2.txt", + ], + ], + }, + }); + ``` + +- Both `deno.permissions` and its children support the option `"inherit"`, which + implies it will borrow its parent permissions. + + ```ts + // This worker will inherit its parent permissions + const worker = new Worker(new URL("./worker.js", import.meta.url).href, { + type: "module", + deno: { + namespace: true, + permissions: "inherit", + }, + }); + ``` + + ```ts + // This worker will inherit only the net permissions of its parent + const worker = new Worker(new URL("./worker.js", import.meta.url).href, { + type: "module", + deno: { + namespace: true, + permissions: { + env: false, + hrtime: false, + net: "inherit", + plugin: false, + read: false, + run: false, + write: false, + }, + }, + }); + ``` + +- Not specifying the `deno.permissions` option or one of its children will cause + the worker to inherit by default. + + ```ts + // This worker will inherit its parent permissions + const worker = new Worker(new URL("./worker.js", import.meta.url).href, { + type: "module", + }); + ``` + + ```ts + // This worker will inherit all the permissions of its parent BUT net + const worker = new Worker(new URL("./worker.js", import.meta.url).href, { + type: "module", + deno: { + namespace: true, + permissions: { + net: false, + }, + }, + }); + ``` + +- You can disable the permissions of the worker all together by passing false to + the `deno.permissions` option. + + ```ts + // This worker will not have any permissions enabled + const worker = new Worker(new URL("./worker.js", import.meta.url).href, { + type: "module", + deno: { + namespace: true, + permissions: false, + }, + }); + ``` diff --git a/runtime/js/11_workers.js b/runtime/js/11_workers.js index 62210dfae..db0bbc3c7 100644 --- a/runtime/js/11_workers.js +++ b/runtime/js/11_workers.js @@ -3,21 +3,24 @@ ((window) => { const core = window.Deno.core; const { Window } = window.__bootstrap.globalInterfaces; - const { log } = window.__bootstrap.util; + const { log, pathFromURL } = window.__bootstrap.util; const { defineEventHandler } = window.__bootstrap.webUtil; + const build = window.__bootstrap.build.build; function createWorker( specifier, hasSourceCode, sourceCode, useDenoNamespace, + permissions, name, ) { return core.jsonOpSync("op_create_worker", { - specifier, hasSourceCode, - sourceCode, name, + permissions, + sourceCode, + specifier, useDenoNamespace, }); } @@ -47,14 +50,122 @@ return JSON.parse(dataJson); } + /** + * @param {string} permission + * @return {boolean} + */ + function parseBooleanPermission( + 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 (!Array.isArray(value) && typeof value !== "boolean") { + throw new Error( + `Expected 'array' or 'boolean' for ${permission} permission, ${typeof value} received`, + ); + //Casts URLs to absolute routes + } else if (Array.isArray(value)) { + value = value.map((route) => { + if (route instanceof URL) { + 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", + plugin = "inherit", + read = "inherit", + run = "inherit", + write = "inherit", + }) { + return { + env: parseBooleanPermission(env, "env"), + hrtime: parseBooleanPermission(hrtime, "hrtime"), + net: parseArrayPermission(net, "net"), + plugin: parseBooleanPermission(plugin, "plugin"), + read: parseArrayPermission(read, "read"), + run: parseBooleanPermission(run, "run"), + write: parseArrayPermission(write, "write"), + }; + } + class Worker extends EventTarget { #id = 0; #name = ""; #terminated = false; - constructor(specifier, options) { + constructor(specifier, options = {}) { super(); - const { type = "classic", name = "unknown" } = options ?? {}; + const { + deno = {}, + name = "unknown", + type = "classic", + } = options; + + // TODO(Soremwar) + // `deno: true` is kept for backwards compatibility with the previous worker + // options implementation. Remove for 2.0 + let workerDenoAttributes; + if (deno === true) { + workerDenoAttributes = { + // Change this to enable the Deno namespace by default + namespace: deno, + permissions: null, + }; + } 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 false, all permissions + // must be removed from the worker + if (workerDenoAttributes.permissions === false) { + workerDenoAttributes.permissions = { + env: false, + hrtime: false, + net: false, + plugin: false, + read: false, + run: false, + write: false, + }; + } + } if (type !== "module") { throw new Error( @@ -66,13 +177,14 @@ const hasSourceCode = false; const sourceCode = decoder.decode(new Uint8Array()); - const useDenoNamespace = options ? !!options.deno : false; - const { id } = createWorker( specifier, hasSourceCode, sourceCode, - useDenoNamespace, + workerDenoAttributes.namespace, + workerDenoAttributes.permissions === null + ? null + : parsePermissions(workerDenoAttributes.permissions), options?.name, ); this.#id = id; diff --git a/runtime/ops/worker_host.rs b/runtime/ops/worker_host.rs index 871e4b9fe..da00c6e6e 100644 --- a/runtime/ops/worker_host.rs +++ b/runtime/ops/worker_host.rs @@ -1,14 +1,22 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use crate::permissions::resolve_fs_allowlist; +use crate::permissions::PermissionState; use crate::permissions::Permissions; +use crate::permissions::UnaryPermission; use crate::web_worker::run_web_worker; use crate::web_worker::WebWorker; use crate::web_worker::WebWorkerHandle; use crate::web_worker::WorkerEvent; +use deno_core::error::custom_error; use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::error::JsError; use deno_core::futures::channel::mpsc; +use deno_core::serde::de; +use deno_core::serde::de::SeqAccess; +use deno_core::serde::Deserialize; +use deno_core::serde::Deserializer; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::serde_json::Value; @@ -16,10 +24,12 @@ use deno_core::BufVec; use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_core::ZeroCopyBuf; -use serde::Deserialize; 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; @@ -27,6 +37,7 @@ use std::thread::JoinHandle; pub struct CreateWebWorkerArgs { pub name: String, pub worker_id: u32, + pub parent_permissions: Permissions, pub permissions: Permissions, pub main_module: ModuleSpecifier, pub use_deno_namespace: bool, @@ -47,6 +58,14 @@ struct HostUnhandledErrorArgs { message: String, } +pub struct WorkerThread { + join_handle: JoinHandle<Result<(), AnyError>>, + worker_handle: WebWorkerHandle, +} + +pub type WorkersTable = HashMap<u32, WorkerThread>; +pub type WorkerId = u32; + pub fn init( rt: &mut deno_core::JsRuntime, sender: Option<mpsc::Sender<WorkerEvent>>, @@ -86,21 +105,348 @@ pub fn init( ); } -pub struct WorkerThread { - join_handle: JoinHandle<Result<(), AnyError>>, - worker_handle: WebWorkerHandle, +fn merge_permission_state( + target: &PermissionState, + incoming: Option<PermissionState>, +) -> Result<PermissionState, AnyError> { + match target { + PermissionState::Granted => match incoming { + Some(x) => Ok(x), + None => Ok(*target), + }, + _ => match incoming { + Some(x) => match x { + PermissionState::Denied => Ok(x), + _ => Err(custom_error( + "PermissionDenied", + "Can't escalate parent thread permissions", + )), + }, + None => Ok(*target), + }, + } } -pub type WorkersTable = HashMap<u32, WorkerThread>; -pub type WorkerId = u32; +fn check_net_permission_contains( + a: &HashSet<String>, + b: &HashSet<String>, +) -> bool { + b.iter().all(|x| a.contains(x)) +} + +fn merge_net_permissions( + target: &UnaryPermission<String>, + incoming: Option<UnaryPermission<String>>, +) -> Result<UnaryPermission<String>, AnyError> { + if incoming.is_none() { + return Ok(target.clone()); + }; + + let new_permissions = incoming.unwrap(); + match &target.global_state { + PermissionState::Granted => Ok(UnaryPermission::<String> { + global_state: new_permissions.global_state, + granted_list: new_permissions.granted_list, + denied_list: new_permissions.denied_list, + }), + PermissionState::Prompt => match new_permissions.global_state { + //Throw + PermissionState::Granted => Err(custom_error( + "PermissionDenied", + "Can't escalate parent thread permissions", + )), + //Merge + PermissionState::Prompt => { + if check_net_permission_contains( + &target.granted_list, + &new_permissions.granted_list, + ) { + Ok(UnaryPermission::<String> { + global_state: new_permissions.global_state, + granted_list: new_permissions.granted_list, + denied_list: target.denied_list.clone(), + }) + } else { + Err(custom_error( + "PermissionDenied", + "Can't escalate parent thread permissions", + )) + } + } + //Copy + PermissionState::Denied => Ok(UnaryPermission::<String> { + global_state: new_permissions.global_state, + granted_list: new_permissions.granted_list, + denied_list: new_permissions.denied_list, + }), + }, + PermissionState::Denied => match new_permissions.global_state { + PermissionState::Denied => Ok(UnaryPermission::<String> { + global_state: new_permissions.global_state, + granted_list: new_permissions.granted_list, + denied_list: new_permissions.denied_list, + }), + _ => Err(custom_error( + "PermissionDenied", + "Can't escalate parent thread permissions", + )), + }, + } +} + +enum WorkerPermissionType { + READ, + WRITE, +} + +fn check_read_permissions( + allow_list: &HashSet<PathBuf>, + current_permissions: &Permissions, +) -> bool { + allow_list + .iter() + .all(|x| current_permissions.check_read(&x).is_ok()) +} + +fn check_write_permissions( + allow_list: &HashSet<PathBuf>, + current_permissions: &Permissions, +) -> bool { + allow_list + .iter() + .all(|x| current_permissions.check_write(&x).is_ok()) +} + +fn merge_read_write_permissions( + permission_type: WorkerPermissionType, + target: &UnaryPermission<PathBuf>, + incoming: Option<UnaryPermission<PathBuf>>, + current_permissions: &Permissions, +) -> Result<UnaryPermission<PathBuf>, AnyError> { + if incoming.is_none() { + return Ok(target.clone()); + }; + + let new_permissions = incoming.unwrap(); + match &target.global_state { + PermissionState::Granted => Ok(UnaryPermission::<PathBuf> { + global_state: new_permissions.global_state, + granted_list: new_permissions.granted_list, + denied_list: new_permissions.denied_list, + }), + PermissionState::Prompt => match new_permissions.global_state { + //Throw + PermissionState::Granted => Err(custom_error( + "PermissionDenied", + "Can't escalate parent thread permissions", + )), + //Merge + PermissionState::Prompt => { + if match permission_type { + WorkerPermissionType::READ => check_read_permissions( + &new_permissions.granted_list, + current_permissions, + ), + WorkerPermissionType::WRITE => check_write_permissions( + &new_permissions.granted_list, + current_permissions, + ), + } { + Ok(UnaryPermission::<PathBuf> { + global_state: new_permissions.global_state, + granted_list: new_permissions.granted_list, + denied_list: target.denied_list.clone(), + }) + } else { + Err(custom_error( + "PermissionDenied", + "Can't escalate parent thread permissions", + )) + } + } + //Copy + PermissionState::Denied => Ok(UnaryPermission::<PathBuf> { + global_state: new_permissions.global_state, + granted_list: new_permissions.granted_list, + denied_list: new_permissions.denied_list, + }), + }, + PermissionState::Denied => match new_permissions.global_state { + PermissionState::Denied => Ok(UnaryPermission::<PathBuf> { + global_state: new_permissions.global_state, + granted_list: new_permissions.granted_list, + denied_list: new_permissions.denied_list, + }), + _ => Err(custom_error( + "PermissionDenied", + "Can't escalate parent thread permissions", + )), + }, + } +} + +fn create_worker_permissions( + main_thread_permissions: &Permissions, + permission_args: PermissionsArg, +) -> Result<Permissions, AnyError> { + Ok(Permissions { + env: merge_permission_state( + &main_thread_permissions.env, + permission_args.env, + )?, + hrtime: merge_permission_state( + &main_thread_permissions.hrtime, + permission_args.hrtime, + )?, + net: merge_net_permissions( + &main_thread_permissions.net, + permission_args.net, + )?, + plugin: merge_permission_state( + &main_thread_permissions.plugin, + permission_args.plugin, + )?, + read: merge_read_write_permissions( + WorkerPermissionType::READ, + &main_thread_permissions.read, + permission_args.read, + &main_thread_permissions, + )?, + run: merge_permission_state( + &main_thread_permissions.run, + permission_args.run, + )?, + write: merge_read_write_permissions( + WorkerPermissionType::WRITE, + &main_thread_permissions.write, + permission_args.write, + &main_thread_permissions, + )?, + }) +} + +#[derive(Debug, Deserialize)] +struct PermissionsArg { + #[serde(default, deserialize_with = "as_permission_state")] + env: Option<PermissionState>, + #[serde(default, deserialize_with = "as_permission_state")] + hrtime: Option<PermissionState>, + #[serde(default, deserialize_with = "as_unary_string_permission")] + net: Option<UnaryPermission<String>>, + #[serde(default, deserialize_with = "as_permission_state")] + plugin: Option<PermissionState>, + #[serde(default, deserialize_with = "as_unary_path_permission")] + read: Option<UnaryPermission<PathBuf>>, + #[serde(default, deserialize_with = "as_permission_state")] + run: Option<PermissionState>, + #[serde(default, deserialize_with = "as_unary_path_permission")] + write: Option<UnaryPermission<PathBuf>>, +} + +fn as_permission_state<'de, D>( + deserializer: D, +) -> Result<Option<PermissionState>, 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<String>, +} + +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") + } + + fn visit_bool<E>(self, v: bool) -> Result<UnaryPermissionBase, E> + where + E: de::Error, + { + Ok(UnaryPermissionBase { + global_state: match v { + true => PermissionState::Granted, + false => PermissionState::Denied, + }, + paths: Vec::new(), + }) + } + + fn visit_seq<V>(self, mut visitor: V) -> Result<UnaryPermissionBase, V::Error> + where + V: SeqAccess<'de>, + { + let mut vec: Vec<String> = Vec::new(); + + let mut value = visitor.next_element::<String>()?; + while value.is_some() { + vec.push(value.unwrap()); + value = visitor.next_element()?; + } + Ok(UnaryPermissionBase { + global_state: PermissionState::Prompt, + paths: vec, + }) + } +} + +fn as_unary_string_permission<'de, D>( + deserializer: D, +) -> Result<Option<UnaryPermission<String>>, D::Error> +where + D: Deserializer<'de>, +{ + let value: UnaryPermissionBase = + deserializer.deserialize_any(ParseBooleanOrStringVec)?; + + let allowed: HashSet<String> = value.paths.into_iter().collect(); + + Ok(Some(UnaryPermission::<String> { + global_state: value.global_state, + granted_list: allowed, + ..Default::default() + })) +} + +fn as_unary_path_permission<'de, D>( + deserializer: D, +) -> Result<Option<UnaryPermission<PathBuf>>, D::Error> +where + D: Deserializer<'de>, +{ + let value: UnaryPermissionBase = + deserializer.deserialize_any(ParseBooleanOrStringVec)?; + + let paths: Vec<PathBuf> = + value.paths.into_iter().map(PathBuf::from).collect(); + + Ok(Some(UnaryPermission::<PathBuf> { + global_state: value.global_state, + granted_list: resolve_fs_allowlist(&Some(paths)), + ..Default::default() + })) +} #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CreateWorkerArgs { - name: Option<String>, - specifier: String, has_source_code: bool, + name: Option<String>, + permissions: Option<PermissionsArg>, source_code: String, + specifier: String, use_deno_namespace: bool, } @@ -121,9 +467,16 @@ fn op_create_worker( let args_name = args.name; let use_deno_namespace = args.use_deno_namespace; if use_deno_namespace { - super::check_unstable(state, "Worker.deno"); + super::check_unstable(state, "Worker.deno.namespace"); } - let permissions = state.borrow::<Permissions>().clone(); + let parent_permissions = state.borrow::<Permissions>().clone(); + let worker_permissions = if let Some(permissions) = args.permissions { + super::check_unstable(state, "Worker.deno.permissions"); + create_worker_permissions(&parent_permissions, permissions)? + } else { + parent_permissions.clone() + }; + let worker_id = state.take::<WorkerId>(); let create_module_loader = state.take::<CreateWebWorkerCbHolder>(); state.put::<CreateWebWorkerCbHolder>(create_module_loader.clone()); @@ -149,7 +502,8 @@ fn op_create_worker( let worker = (create_module_loader.0)(CreateWebWorkerArgs { name: worker_name, worker_id, - permissions, + parent_permissions, + permissions: worker_permissions, main_module: module_specifier.clone(), use_deno_namespace, }); diff --git a/runtime/permissions.rs b/runtime/permissions.rs index c50783f9d..16a611690 100644 --- a/runtime/permissions.rs +++ b/runtime/permissions.rs @@ -78,7 +78,7 @@ pub struct Permissions { pub hrtime: PermissionState, } -fn resolve_fs_allowlist(allow: &Option<Vec<PathBuf>>) -> HashSet<PathBuf> { +pub fn resolve_fs_allowlist(allow: &Option<Vec<PathBuf>>) -> HashSet<PathBuf> { if let Some(v) = allow { v.iter() .map(|raw_path| resolve_from_cwd(Path::new(&raw_path)).unwrap()) |
