diff --git a/BUILD.gn b/BUILD.gn index 756d0e9cf5..91abcc80d5 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -91,6 +91,7 @@ ts_sources = [ "js/mock_builtin.js", "js/net.ts", "js/os.ts", + "js/permissions.ts", "js/platform.ts", "js/plugins.d.ts", "js/process.ts", diff --git a/js/deno.ts b/js/deno.ts index 66d7d796c7..d13ca81dd2 100644 --- a/js/deno.ts +++ b/js/deno.ts @@ -50,6 +50,12 @@ export { symlinkSync, symlink } from "./symlink"; export { writeFileSync, writeFile, WriteFileOptions } from "./write_file"; export { ErrorKind, DenoError } from "./errors"; export { libdeno } from "./libdeno"; +export { + permissions, + revokePermission, + Permission, + Permissions +} from "./permissions"; export { platform } from "./platform"; export { truncateSync, truncate } from "./truncate"; export { FileInfo } from "./file_info"; diff --git a/js/permissions.ts b/js/permissions.ts new file mode 100644 index 0000000000..6acb80b1f9 --- /dev/null +++ b/js/permissions.ts @@ -0,0 +1,74 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import * as msg from "gen/msg_generated"; +import * as flatbuffers from "./flatbuffers"; +import * as dispatch from "./dispatch"; +import { assert } from "./util"; + +/** Permissions as granted by the caller */ +export type Permissions = { + read: boolean; + write: boolean; + net: boolean; + env: boolean; + run: boolean; + + // NOTE: Keep in sync with src/permissions.rs +}; + +export type Permission = keyof Permissions; + +/** Inspect granted permissions for the current program. + * + * if (Deno.permissions().read) { + * const file = await Deno.readFile("example.test"); + * // ... + * } + */ +export function permissions(): Permissions { + const baseRes = dispatch.sendSync(...getReq())!; + assert(msg.Any.PermissionsRes === baseRes.innerType()); + const res = new msg.PermissionsRes(); + assert(baseRes.inner(res) != null); + // TypeScript cannot track assertion above, therefore not null assertion + return createPermissions(res); +} + +/** Revoke a permission. When the permission was already revoked nothing changes + * + * if (Deno.permissions().read) { + * const file = await Deno.readFile("example.test"); + * Deno.revokePermission('read'); + * } + * Deno.readFile("example.test"); // -> error or permission prompt + */ +export function revokePermission(permission: Permission): void { + dispatch.sendSync(...revokeReq(permission)); +} + +function createPermissions(inner: msg.PermissionsRes): Permissions { + return { + read: inner.read(), + write: inner.write(), + net: inner.net(), + env: inner.env(), + run: inner.run() + }; +} + +function getReq(): [flatbuffers.Builder, msg.Any, flatbuffers.Offset] { + const builder = flatbuffers.createBuilder(); + msg.Permissions.startPermissions(builder); + const inner = msg.Permissions.endPermissions(builder); + return [builder, msg.Any.Permissions, inner]; +} + +function revokeReq( + permission: string +): [flatbuffers.Builder, msg.Any, flatbuffers.Offset] { + const builder = flatbuffers.createBuilder(); + const permission_ = builder.createString(permission); + msg.PermissionRevoke.startPermissionRevoke(builder); + msg.PermissionRevoke.addPermission(builder, permission_); + const inner = msg.PermissionRevoke.endPermissionRevoke(builder); + return [builder, msg.Any.PermissionRevoke, inner]; +} diff --git a/js/permissions_test.ts b/js/permissions_test.ts new file mode 100644 index 0000000000..f4245c03b7 --- /dev/null +++ b/js/permissions_test.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { testPerm, assert, assertEqual } from "./test_util.ts"; +import { Permission } from "deno"; + +const knownPermissions: Permission[] = ["run", "read", "write", "net", "env"]; + +for (let grant of knownPermissions) { + testPerm({ [grant]: true }, function envGranted() { + const perms = Deno.permissions(); + assert(perms !== null); + for (const perm in perms) { + assertEqual(perms[perm], perm === grant); + } + + Deno.revokePermission(grant); + + const revoked = Deno.permissions(); + for (const perm in revoked) { + assertEqual(revoked[perm], false); + } + }); +} diff --git a/js/unit_tests.ts b/js/unit_tests.ts index 91c1745b6b..5085deb5ca 100644 --- a/js/unit_tests.ts +++ b/js/unit_tests.ts @@ -45,6 +45,7 @@ import "./url_test.ts"; import "./url_search_params_test.ts"; import "./write_file_test.ts"; import "./performance_test.ts"; +import "./permissions_test.ts"; import "./version_test.ts"; import "../website/app_test.js"; diff --git a/src/msg.fbs b/src/msg.fbs index da03e00a4e..1b57e72ef5 100644 --- a/src/msg.fbs +++ b/src/msg.fbs @@ -12,6 +12,9 @@ union Any { Exit, Environ, EnvironRes, + Permissions, + PermissionRevoke, + PermissionsRes, Fetch, FetchRes, MakeTempDir, @@ -231,6 +234,20 @@ table KeyValue { value: string; } +table Permissions {} + +table PermissionRevoke { + permission: string; +} + +table PermissionsRes { + run: bool; + read: bool; + write: bool; + net: bool; + env: bool; +} + // Note this represents The WHOLE header of an http message, not just the key // value pairs. That means it includes method and url for Requests and status // for responses. This is why it is singular "Header" instead of "Headers". diff --git a/src/ops.rs b/src/ops.rs index 5535ca1b90..ba309ca7ab 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -130,6 +130,8 @@ pub fn dispatch( msg::Any::Now => op_now, msg::Any::IsTTY => op_is_tty, msg::Any::Seek => op_seek, + msg::Any::Permissions => op_permissions, + msg::Any::PermissionRevoke => op_revoke_permission, _ => panic!(format!( "Unhandled message {}", msg::enum_name_any(inner_type) @@ -503,6 +505,57 @@ fn op_env( )) } +fn op_permissions( + isolate: &Isolate, + base: &msg::Base<'_>, + data: libdeno::deno_buf, +) -> Box { + assert_eq!(data.len(), 0); + let cmd_id = base.cmd_id(); + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::PermissionsRes::create( + builder, + &msg::PermissionsResArgs { + run: isolate.permissions.allows_run(), + read: isolate.permissions.allows_read(), + write: isolate.permissions.allows_write(), + net: isolate.permissions.allows_net(), + env: isolate.permissions.allows_env(), + }, + ); + ok_future(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::PermissionsRes, + ..Default::default() + }, + )) +} + +fn op_revoke_permission( + isolate: &Isolate, + base: &msg::Base<'_>, + data: libdeno::deno_buf, +) -> Box { + assert_eq!(data.len(), 0); + let inner = base.inner_as_permission_revoke().unwrap(); + let permission = inner.permission().unwrap(); + let result = match permission { + "run" => isolate.permissions.revoke_run(), + "read" => isolate.permissions.revoke_read(), + "write" => isolate.permissions.revoke_write(), + "net" => isolate.permissions.revoke_net(), + "env" => isolate.permissions.revoke_env(), + _ => Ok(()), + }; + if let Err(e) = result { + return odd_future(e); + } + ok_future(empty_buf()) +} + fn op_fetch( isolate: &Isolate, base: &msg::Base<'_>, diff --git a/src/permissions.rs b/src/permissions.rs index b40afb64e8..03ffd20cb5 100644 --- a/src/permissions.rs +++ b/src/permissions.rs @@ -12,6 +12,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; #[cfg_attr(feature = "cargo-clippy", allow(stutter))] #[derive(Debug, Default)] pub struct DenoPermissions { + // Keep in sync with src/permissions.ts pub allow_read: AtomicBool, pub allow_write: AtomicBool, pub allow_net: AtomicBool, @@ -91,6 +92,51 @@ impl DenoPermissions { r } + pub fn allows_run(&self) -> bool { + return self.allow_run.load(Ordering::SeqCst); + } + + pub fn allows_read(&self) -> bool { + return self.allow_read.load(Ordering::SeqCst); + } + + pub fn allows_write(&self) -> bool { + return self.allow_write.load(Ordering::SeqCst); + } + + pub fn allows_net(&self) -> bool { + return self.allow_net.load(Ordering::SeqCst); + } + + pub fn allows_env(&self) -> bool { + return self.allow_env.load(Ordering::SeqCst); + } + + pub fn revoke_run(&self) -> DenoResult<()> { + self.allow_run.store(false, Ordering::SeqCst); + return Ok(()); + } + + pub fn revoke_read(&self) -> DenoResult<()> { + self.allow_read.store(false, Ordering::SeqCst); + return Ok(()); + } + + pub fn revoke_write(&self) -> DenoResult<()> { + self.allow_write.store(false, Ordering::SeqCst); + return Ok(()); + } + + pub fn revoke_net(&self) -> DenoResult<()> { + self.allow_net.store(false, Ordering::SeqCst); + return Ok(()); + } + + pub fn revoke_env(&self) -> DenoResult<()> { + self.allow_env.store(false, Ordering::SeqCst); + return Ok(()); + } + pub fn default() -> Self { Self { allow_read: AtomicBool::new(false), diff --git a/website/manual.md b/website/manual.md index 9081637a16..43b79d003f 100644 --- a/website/manual.md +++ b/website/manual.md @@ -286,6 +286,35 @@ It's worth noting that like the `cat.ts` example, the `copy()` function here also does not make unnecessary memory copies. It receives a packet from the kernel and sends back, without further complexity. +### Inspecting and revoking permissions + +Sometimes a program may want to revoke previously granted permissions. When a +program, at a later stage, needs those permissions, a new prompt will be +presented to the user. + +```ts +const { permissions, revokePermission, open, remove } = Deno; + +(async () => { + // lookup a permission + if (!permissions().write) { + throw new Error("need write permission"); + } + + const log = await open("request.log", "a+"); + + // revoke some permissions + revokePermission("read"); + revokePermission("write"); + + // use the log file + await log.write(encoder.encode("hello\n")); + + // this will prompt for the write permission or fail. + await remove("request.log"); +})(); +``` + ### File server This one serves a local directory in HTTP.