1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

refactor: permissions (#7074)

This commit is contained in:
Nayeem Rahman 2020-08-18 21:29:32 +01:00 committed by GitHub
parent f6e9150b33
commit 015fa0bd41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1051 additions and 724 deletions

2
Cargo.lock generated
View file

@ -346,7 +346,6 @@ dependencies = [
"nix",
"notify",
"os_pipe",
"pty",
"rand 0.7.3",
"regex",
"reqwest",
@ -2348,6 +2347,7 @@ dependencies = [
"futures",
"lazy_static",
"os_pipe",
"pty",
"regex",
"tempfile",
"tokio",

View file

@ -83,9 +83,6 @@ os_pipe = "0.9.2"
tokio-tungstenite = { version = "0.10.1", features = ["connect"] }
test_util = { path = "../test_util" }
[target.'cfg(unix)'.dev-dependencies]
pty = "0.2.2"
[package.metadata.winres]
# This section defines the metadata that appears in the deno.exe PE header.
OriginalFilename = "deno.exe"

View file

@ -174,7 +174,13 @@ impl OpError {
}
pub fn invalid_domain_error() -> OpError {
OpError::new(ErrorKind::TypeError, "Invalid domain.".to_string())
OpError::type_error("Invalid domain.".to_string())
}
pub fn permission_escalation_error() -> OpError {
OpError::permission_denied(
"Arguments escalate parent permissions.".to_string(),
)
}
}

View file

@ -35,12 +35,18 @@ pub fn op_query_permission(
) -> Result<JsonOp, OpError> {
let args: PermissionArgs = serde_json::from_value(args)?;
let state = state.borrow();
let permissions = &state.permissions;
let path = args.path.as_deref();
let perm = state.permissions.get_permission_state(
&args.name,
&args.url.as_deref(),
&path.as_deref().map(Path::new),
)?;
let perm = match args.name.as_ref() {
"read" => permissions.query_read(&path.as_deref().map(Path::new)),
"write" => permissions.query_write(&path.as_deref().map(Path::new)),
"net" => permissions.query_net_url(&args.url.as_deref())?,
"env" => permissions.query_env(),
"run" => permissions.query_run(),
"plugin" => permissions.query_plugin(),
"hrtime" => permissions.query_hrtime(),
n => return Err(OpError::other(format!("No such permission name: {}", n))),
};
Ok(JsonOp::Sync(json!({ "state": perm.to_string() })))
}
@ -52,22 +58,17 @@ pub fn op_revoke_permission(
let args: PermissionArgs = serde_json::from_value(args)?;
let mut state = state.borrow_mut();
let permissions = &mut state.permissions;
match args.name.as_ref() {
"run" => permissions.allow_run.revoke(),
"read" => permissions.allow_read.revoke(),
"write" => permissions.allow_write.revoke(),
"net" => permissions.allow_net.revoke(),
"env" => permissions.allow_env.revoke(),
"plugin" => permissions.allow_plugin.revoke(),
"hrtime" => permissions.allow_hrtime.revoke(),
_ => {}
};
let path = args.path.as_deref();
let perm = permissions.get_permission_state(
&args.name,
&args.url.as_deref(),
&path.as_deref().map(Path::new),
)?;
let perm = match args.name.as_ref() {
"read" => permissions.revoke_read(&path.as_deref().map(Path::new)),
"write" => permissions.revoke_write(&path.as_deref().map(Path::new)),
"net" => permissions.revoke_net(&args.url.as_deref())?,
"env" => permissions.revoke_env(),
"run" => permissions.revoke_run(),
"plugin" => permissions.revoke_plugin(),
"hrtime" => permissions.revoke_hrtime(),
n => return Err(OpError::other(format!("No such permission name: {}", n))),
};
Ok(JsonOp::Sync(json!({ "state": perm.to_string() })))
}
@ -81,14 +82,14 @@ pub fn op_request_permission(
let permissions = &mut state.permissions;
let path = args.path.as_deref();
let perm = match args.name.as_ref() {
"run" => Ok(permissions.request_run()),
"read" => Ok(permissions.request_read(&path.as_deref().map(Path::new))),
"write" => Ok(permissions.request_write(&path.as_deref().map(Path::new))),
"net" => permissions.request_net(&args.url.as_deref()),
"env" => Ok(permissions.request_env()),
"plugin" => Ok(permissions.request_plugin()),
"hrtime" => Ok(permissions.request_hrtime()),
n => Err(OpError::other(format!("No such permission name: {}", n))),
}?;
"read" => permissions.request_read(&path.as_deref().map(Path::new)),
"write" => permissions.request_write(&path.as_deref().map(Path::new)),
"net" => permissions.request_net(&args.url.as_deref())?,
"env" => permissions.request_env(),
"run" => permissions.request_run(),
"plugin" => permissions.request_plugin(),
"hrtime" => permissions.request_hrtime(),
n => return Err(OpError::other(format!("No such permission name: {}", n))),
};
Ok(JsonOp::Sync(json!({ "state": perm.to_string() })))
}

View file

@ -59,16 +59,20 @@ fn op_now(
_args: Value,
_zero_copy: &mut [ZeroCopyBuf],
) -> Result<JsonOp, OpError> {
let state = state.borrow();
let seconds = state.start_time.elapsed().as_secs();
let mut subsec_nanos = state.start_time.elapsed().subsec_nanos();
let inner_state = state.borrow();
let seconds = inner_state.start_time.elapsed().as_secs();
let mut subsec_nanos = inner_state.start_time.elapsed().subsec_nanos();
let reduced_time_precision = 2_000_000; // 2ms in nanoseconds
// If the permission is not enabled
// Round the nano result on 2 milliseconds
// see: https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#Reduced_time_precision
if !state.permissions.allow_hrtime.is_allow() {
subsec_nanos -= subsec_nanos % reduced_time_precision
if let Err(op_error) = state.check_hrtime() {
if op_error.kind_str == "PermissionDenied" {
subsec_nanos -= subsec_nanos % reduced_time_precision;
} else {
return Err(op_error);
}
}
Ok(JsonOp::Sync(json!({

File diff suppressed because it is too large Load diff

View file

@ -560,6 +560,11 @@ impl State {
self.borrow().permissions.check_run()
}
#[inline]
pub fn check_hrtime(&self) -> Result<(), OpError> {
self.borrow().permissions.check_hrtime()
}
#[inline]
pub fn check_plugin(&self, filename: &Path) -> Result<(), OpError> {
self.borrow().permissions.check_plugin(filename)

View file

@ -1,11 +0,0 @@
[WILDCARD]
running 7 tests
test runGranted ... ok [WILDCARD]
test readGranted ... ok [WILDCARD]
test writeGranted ... ok [WILDCARD]
test netGranted ... ok [WILDCARD]
test envGranted ... ok [WILDCARD]
test pluginGranted ... ok [WILDCARD]
test hrtimeGranted ... ok [WILDCARD]
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out [WILDCARD]

View file

@ -1,36 +0,0 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
const knownPermissions: Deno.PermissionName[] = [
"run",
"read",
"write",
"net",
"env",
"plugin",
"hrtime",
];
export function assert(cond: unknown): asserts cond {
if (!cond) {
throw Error("Assertion failed");
}
}
function genFunc(grant: Deno.PermissionName): [string, () => Promise<void>] {
const gen: () => Promise<void> = async function Granted(): Promise<void> {
const status0 = await Deno.permissions.query({ name: grant });
assert(status0 != null);
assert(status0.state === "granted");
const status1 = await Deno.permissions.revoke({ name: grant });
assert(status1 != null);
assert(status1.state === "prompt");
};
const name = grant + "Granted";
return [name, gen];
}
for (const grant of knownPermissions) {
const [name, fn] = genFunc(grant);
Deno.test(name, fn);
}

View file

@ -0,0 +1,6 @@
const status1 = await Deno.permissions.request({ name: "read", path: "foo" });
const status2 = await Deno.permissions.query({ name: "read", path: "bar" });
const status3 = await Deno.permissions.request({ name: "read", path: "bar" });
console.log(status1);
console.log(status2);
console.log(status3);

View file

@ -0,0 +1,3 @@
[WILDCARD]PermissionStatus { state: "granted" }
PermissionStatus { state: "prompt" }
PermissionStatus { state: "denied" }

View file

@ -0,0 +1,6 @@
const status1 = await Deno.permissions.request({ name: "read" });
const status2 = await Deno.permissions.query({ name: "read", path: "foo" });
const status3 = await Deno.permissions.query({ name: "read", path: "bar" });
console.log(status1);
console.log(status2);
console.log(status3);

View file

@ -0,0 +1,3 @@
[WILDCARD]PermissionStatus { state: "granted" }
PermissionStatus { state: "granted" }
PermissionStatus { state: "granted" }

View file

@ -0,0 +1,6 @@
const status1 = await Deno.permissions.revoke({ name: "read", path: "foo" });
const status2 = await Deno.permissions.query({ name: "read", path: "bar" });
const status3 = await Deno.permissions.revoke({ name: "read", path: "bar" });
console.log(status1);
console.log(status2);
console.log(status3);

View file

@ -0,0 +1,3 @@
[WILDCARD]PermissionStatus { state: "prompt" }
PermissionStatus { state: "granted" }
PermissionStatus { state: "prompt" }

View file

@ -0,0 +1,6 @@
const status1 = await Deno.permissions.revoke({ name: "read" });
const status2 = await Deno.permissions.query({ name: "read", path: "foo" });
const status3 = await Deno.permissions.query({ name: "read", path: "bar" });
console.log(status1);
console.log(status2);
console.log(status3);

View file

@ -0,0 +1,3 @@
[WILDCARD]PermissionStatus { state: "prompt" }
PermissionStatus { state: "prompt" }
PermissionStatus { state: "prompt" }

View file

@ -1,8 +1,6 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
#[cfg(unix)]
extern crate nix;
#[cfg(unix)]
extern crate pty;
extern crate tempfile;
use test_util as util;
@ -166,8 +164,8 @@ fn no_color() {
#[test]
#[ignore]
pub fn test_raw_tty() {
use pty::fork::*;
use std::io::{Read, Write};
use util::pty::fork::*;
let fork = Fork::from_ptmx().unwrap();
@ -1581,12 +1579,6 @@ itest!(_056_make_temp_file_write_perm {
output: "056_make_temp_file_write_perm.out",
});
// TODO(lucacasonato): remove --unstable when permissions goes stable
itest!(_057_revoke_permissions {
args: "test -A --unstable 057_revoke_permissions.ts",
output: "057_revoke_permissions.out",
});
itest!(_058_tasks_microtasks_close {
args: "run --quiet 058_tasks_microtasks_close.ts",
output: "058_tasks_microtasks_close.ts.out",
@ -1603,6 +1595,36 @@ itest!(_060_deno_doc_displays_all_overloads_in_details_view {
output: "060_deno_doc_displays_all_overloads_in_details_view.ts.out",
});
#[cfg(unix)]
#[test]
fn _061_permissions_request() {
let args = "run --unstable 061_permissions_request.ts";
let output = "061_permissions_request.ts.out";
let input = b"g\nd\n";
util::test_pty(args, output, input);
}
#[cfg(unix)]
#[test]
fn _062_permissions_request_global() {
let args = "run --unstable 062_permissions_request_global.ts";
let output = "062_permissions_request_global.ts.out";
let input = b"g\n";
util::test_pty(args, output, input);
}
itest!(_063_permissions_revoke {
args: "run --unstable --allow-read=foo,bar 063_permissions_revoke.ts",
output: "063_permissions_revoke.ts.out",
});
itest!(_064_permissions_revoke_global {
args: "run --unstable --allow-read=foo,bar 064_permissions_revoke_global.ts",
output: "064_permissions_revoke_global.ts.out",
});
itest!(js_import_detect {
args: "run --quiet --reload js_import_detect.ts",
output: "js_import_detect.ts.out",

View file

@ -1,28 +0,0 @@
## Inspecting and revoking permissions
> This program makes use of an unstable Deno feature. Learn more about
> [unstable features](../runtime/stability.md).
Sometimes a program may want to revoke previously granted permissions. When a
program, at a later stage, needs those permissions, it will fail.
```ts
// lookup a permission
const status = await Deno.permissions.query({ name: "write" });
if (status.state !== "granted") {
throw new Error("need write permission");
}
const log = await Deno.open("request.log", { write: true, append: true });
// revoke some permissions
await Deno.permissions.revoke({ name: "read" });
await Deno.permissions.revoke({ name: "write" });
// use the log file
const encoder = new TextEncoder();
await log.write(encoder.encode("hello\n"));
// this will fail.
await Deno.remove("request.log");
```

View file

@ -1,7 +1,7 @@
## Compiler API
## Compiler APIs
> This is an unstable Deno feature. Learn more about
> [unstable features](./stability.md).
> This API is unstable. Learn more about
> [unstable features](../runtime/stability.md).
Deno supports runtime access to the built-in TypeScript compiler. There are
three methods in the `Deno` namespace that provide this access.

View file

@ -0,0 +1,189 @@
## Permission APIs
> This API is unstable. Learn more about
> [unstable features](../runtime/stability.md).
Permissions are granted from the CLI when running the `deno` command. User code
will often assume its own set of required permissions, but there is no guarantee
during execution that the set of _granted_ permissions will align with this.
In some cases, ensuring a fault-tolerant program requires a way to interact with
the permission system at runtime.
### Permission descriptors
On the CLI, read permission for `/foo/bar` is represented as
`--allow-read=/foo/bar`. In runtime JS, it is represented as the following:
```ts
const desc = { name: "read", path: "/foo/bar" };
```
Other examples:
```ts
// Global write permission.
const desc1 = { name: "write" };
// Write permission to `$PWD/foo/bar`.
const desc2 = { name: "write", path: "foo/bar" };
// Global net permission.
const desc3 = { name: "net" };
// Net permission to 127.0.0.1:8000.
const desc4 = { name: "net", url: "127.0.0.1:8000" };
// High-resolution time permission.
const desc5 = { name: "hrtime" };
```
### Query permissions
Check, by descriptor, if a permission is granted or not.
```ts
// deno run --unstable --allow-read=/foo main.ts
const desc1 = { name: "read", path: "/foo" };
console.log(await Deno.permissions.query(desc1));
// PermissionStatus { state: "granted" }
const desc2 = { name: "read", path: "/foo/bar" };
console.log(await Deno.permissions.query(desc2));
// PermissionStatus { state: "granted" }
const desc3 = { name: "read", path: "/bar" };
console.log(await Deno.permissions.query(desc3));
// PermissionStatus { state: "prompt" }
```
### Permission states
A permission state can be either "granted", "prompt" or "denied". Permissions
which have been granted from the CLI will query to `{ state: "granted" }`. Those
which have not been granted query to `{ state: "prompt" }` by default, while
`{ state: "denied" }` reserved for those which have been explicitly refused.
This will come up in [Request permissions](#request-permissions).
### Permission strength
The intuitive understanding behind the result of the second query in
[Query permissions](#query-permissions) is that read access was granted to
`/foo` and `/foo/bar` is within `/foo` so `/foo/bar` is allowed to be read.
We can also say that `desc1` is
_[stronger than](https://www.w3.org/TR/permissions/#ref-for-permissiondescriptor-stronger-than)_
`desc2`. This means that for any set of CLI-granted permissions:
1. If `desc1` queries to `{ state: "granted" }` then so must `desc2`.
2. If `desc2` queries to `{ state: "denied" }` then so must `desc1`.
More examples:
```ts
const desc1 = { name: "write" };
// is stronger than
const desc2 = { name: "write", path: "/foo" };
const desc3 = { name: "net" };
// is stronger than
const desc4 = { name: "net", url: "127.0.0.1:8000" };
```
### Request permissions
Request an ungranted permission from the user via CLI prompt.
```ts
// deno run --unstable main.ts
const desc1 = { name: "read", path: "/foo" };
const status1 = await Deno.permissions.request(desc1);
// ⚠️ Deno requests read access to "/foo". Grant? [g/d (g = grant, d = deny)] g
console.log(status1);
// PermissionStatus { state: "granted" }
const desc2 = { name: "read", path: "/bar" };
const status2 = await Deno.permissions.request(desc2);
// ⚠️ Deno requests read access to "/bar". Grant? [g/d (g = grant, d = deny)] d
console.log(status2);
// PermissionStatus { state: "denied" }
```
If the current permission state is "prompt", a prompt will appear on the user's
terminal asking them if they would like to grant the request. The request for
`desc1` was granted so its new status is returned and execution will continue as
if `--allow-read=/foo` was specified on the CLI. The request for `desc2` was
denied so its permission state is downgraded from "prompt" to "denied".
If the current permission state is already either "granted" or "denied", the
request will behave like a query and just return the current status. This
prevents prompts both for already granted permissions and previously denied
requests.
### Revoke permissions
Downgrade a permission from "granted" to "prompt".
```ts
// deno run --unstable --allow-read=/foo main.ts
const desc = { name: "read", path: "/foo" };
console.log(await Deno.permissions.revoke(desc));
// PermissionStatus { state: "prompt" }
```
However, what happens when you try to revoke a permission which is _partial_ to
one granted on the CLI?
```ts
// deno run --unstable --allow-read=/foo main.ts
const desc = { name: "read", path: "/foo/bar" };
console.log(await Deno.permissions.revoke(desc));
// PermissionStatus { state: "granted" }
```
It was not revoked.
To understand this behaviour, imagine that Deno stores an internal set of
_explicitly granted permission descriptors_. Specifying `--allow-read=/foo,/bar`
on the CLI initializes this set to:
```ts
[
{ name: "read", path: "/foo" },
{ name: "read", path: "/bar" },
];
```
Granting a runtime request for `{ name: "write", path: "/foo" }` updates the set
to:
```ts
[
{ name: "read", path: "/foo" },
{ name: "read", path: "/bar" },
{ name: "write", path: "/foo" },
];
```
Deno's permission revocation algorithm works by removing every element from this
set which the argument permission descriptor is _stronger than_. So to ensure
`desc` is not longer granted, pass an argument descriptor _stronger than_
whichever _explicitly granted permission descriptor_ is _stronger than_ `desc`.
```ts
// deno run --unstable --allow-read=/foo main.ts
const desc = { name: "read", path: "/foo/bar" };
console.log(await Deno.permissions.revoke(desc)); // Insufficient.
// PermissionStatus { state: "granted" }
const strongDesc = { name: "read", path: "/foo" };
await Deno.permissions.revoke(strongDesc); // Good.
console.log(await Deno.permissions.query(desc));
// PermissionStatus { state: "prompt" }
```

View file

@ -19,6 +19,7 @@
"children": {
"stability": "Stability",
"program_lifecycle": "Program lifecycle",
"permission_apis": "Permission APIs",
"compiler_apis": "Compiler APIs",
"workers": "Workers"
}
@ -75,7 +76,6 @@
"file_server": "File server",
"tcp_echo": "TCP echo server",
"subprocess": "Creating a subprocess",
"permissions": "Inspecting and revoking permissions",
"os_signals": "OS Signals",
"file_system_events": "File system events",
"testing_if_main": "Checking if file is main"

View file

@ -18,3 +18,6 @@ os_pipe = "0.9.2"
regex = "1.3.9"
tempfile = "3.1.0"
warp = { version = "0.2.4", features = ["tls"] }
[target.'cfg(unix)'.dependencies]
pty = "0.2.2"

View file

@ -7,6 +7,8 @@ extern crate lazy_static;
use futures::future::{self, FutureExt};
use os_pipe::pipe;
#[cfg(unix)]
pub use pty;
use regex::Regex;
use std::env;
use std::io::Read;
@ -767,7 +769,7 @@ impl CheckOutputIntegrationTest {
}
}
fn wildcard_match(pattern: &str, s: &str) -> bool {
pub fn wildcard_match(pattern: &str, s: &str) -> bool {
pattern_match(pattern, s, "[WILDCARD]")
}
@ -820,6 +822,39 @@ pub fn pattern_match(pattern: &str, s: &str, wildcard: &str) -> bool {
t.1.is_empty()
}
/// Kind of reflects `itest!()`. Note that the pty's output (which also contains
/// stdin content) is compared against the content of the `output` path.
#[cfg(unix)]
pub fn test_pty(args: &str, output_path: &str, input: &[u8]) {
use pty::fork::Fork;
let tests_path = tests_path();
let fork = Fork::from_ptmx().unwrap();
if let Ok(mut master) = fork.is_parent() {
let mut output_actual = String::new();
master.write_all(input).unwrap();
master.read_to_string(&mut output_actual).unwrap();
fork.wait().unwrap();
let output_expected =
std::fs::read_to_string(tests_path.join(output_path)).unwrap();
if !wildcard_match(&output_expected, &output_actual) {
println!("OUTPUT\n{}\nOUTPUT", output_actual);
println!("EXPECTED\n{}\nEXPECTED", output_expected);
panic!("pattern match failed");
}
} else {
deno_cmd()
.current_dir(tests_path)
.env("NO_COLOR", "1")
.args(args.split_whitespace())
.spawn()
.unwrap()
.wait()
.unwrap();
}
}
#[test]
fn test_wildcard_match() {
let fixtures = vec![