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).
This commit is contained in:
Steven Guerrero 2021-01-06 15:31:16 -05:00 committed by GitHub
parent 2e18fcebcc
commit adc2f08c17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1062 additions and 73 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
onmessage = async () => {
const { state } = await Deno.permissions.query({
name: "read",
});
postMessage(state === "granted");
close();
};

View file

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

View file

@ -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 => {

View file

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

View file

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

View file

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

View file

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

View file

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