Skip to content
Permalink
Browse files

Allow inspection and revocation of permissions (#1875)

  • Loading branch information...
fd authored and ry committed Mar 4, 2019
1 parent 048a8a7 commit 77d7ad61f39641b79a60a99da2f939cbc1d8fe39
Showing with 249 additions and 0 deletions.
  1. +1 −0 BUILD.gn
  2. +6 −0 js/deno.ts
  3. +74 −0 js/permissions.ts
  4. +22 −0 js/permissions_test.ts
  5. +1 −0 js/unit_tests.ts
  6. +17 −0 src/msg.fbs
  7. +53 −0 src/ops.rs
  8. +46 −0 src/permissions.rs
  9. +29 −0 website/manual.md
@@ -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",
@@ -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";
@@ -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];
}
@@ -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);
}
});
}
@@ -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";
@@ -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".
@@ -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<Op> {
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<Op> {
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<'_>,
@@ -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),
@@ -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.

0 comments on commit 77d7ad6

Please sign in to comment.
You can’t perform that action at this time.