diff options
Diffstat (limited to 'runtime')
| -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 |
3 files changed, 486 insertions, 20 deletions
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()) |
