Skip to content

Commit

Permalink
feat: Add configurable permissions for Workers (#8215)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
Soremwar authored Jan 6, 2021
1 parent 2e18fce commit adc2f08
Show file tree
Hide file tree
Showing 33 changed files with 1,062 additions and 73 deletions.
56 changes: 44 additions & 12 deletions cli/dts/lib.deno.shared_globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
*
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
22 changes: 20 additions & 2 deletions cli/module_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}

Expand All @@ -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 {
Expand All @@ -52,6 +60,7 @@ impl CliModuleLoader {
Rc::new(CliModuleLoader {
import_map: None,
lib,
initial_permissions: Rc::new(RefCell::new(Some(permissions))),
program_state,
})
}
Expand Down Expand Up @@ -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);

Expand Down
6 changes: 4 additions & 2 deletions cli/tests/unstable_worker.ts
Original file line number Diff line number Diff line change
@@ -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",
},
);
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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!");
Expand Down
File renamed without changes.
File renamed without changes.
17 changes: 17 additions & 0 deletions cli/tests/workers/no_permissions_worker.js
Original file line number Diff line number Diff line change
@@ -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();
};
File renamed without changes.
43 changes: 43 additions & 0 deletions cli/tests/workers/parent_read_check_granular_worker.js
Original file line number Diff line number Diff line change
@@ -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,
});
};
27 changes: 27 additions & 0 deletions cli/tests/workers/parent_read_check_worker.js
Original file line number Diff line number Diff line change
@@ -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);
};
File renamed without changes.
13 changes: 13 additions & 0 deletions cli/tests/workers/read_check_granular_worker.js
Original file line number Diff line number Diff line change
@@ -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,
});
};
7 changes: 7 additions & 0 deletions cli/tests/workers/read_check_worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
onmessage = async () => {
const { state } = await Deno.permissions.query({
name: "read",
});
postMessage(state === "granted");
close();
};
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion cli/tests/workers_round_robin_bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion cli/tests/workers_startup_bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
Loading

0 comments on commit adc2f08

Please sign in to comment.