0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-10-31 09:14:20 -04:00
denoland-deno/docs/runtime/permission_apis.md
Nayeem Rahman 22e0ee92a6
BREAKING(unstable): Use hosts for net allowlists (#8845)
Allowlist checking already uses hosts but for some reason 
requests, revokes and the runtime permissions API use URLs.

- BREAKING(lib.deno.unstable.d.ts): Change 
NetPermissionDescriptor::url to NetPermissionDescriptor::host

- fix(runtime/permissions): Don't add whole URLs to the 
allowlist on request

- fix(runtime/permissions): Harden strength semantics:
({ name: "net", host: "127.0.0.1" } is stronger than 
{ name: "net", host: "127.0.0.1:8000" }) for blocklisting

- refactor(runtime/permissions): Use tuples for hosts, make 
the host optional in Permissions::{query_net, request_net, revoke_net}()
2020-12-30 23:35:28 +01:00

6 KiB

Permission APIs

This API is unstable. Learn more about unstable features.

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:

const desc = { name: "read", path: "/foo/bar" } as const;

Other examples:

// Global write permission.
const desc1 = { name: "write" } as const;

// Write permission to `$PWD/foo/bar`.
const desc2 = { name: "write", path: "foo/bar" } as const;

// Global net permission.
const desc3 = { name: "net" } as const;

// Net permission to 127.0.0.1:8000.
const desc4 = { name: "net", host: "127.0.0.1:8000" } as const;

// High-resolution time permission.
const desc5 = { name: "hrtime" } as const;

Query permissions

Check, by descriptor, if a permission is granted or not.

// deno run --unstable --allow-read=/foo main.ts

const desc1 = { name: "read", path: "/foo" } as const;
console.log(await Deno.permissions.query(desc1));
// PermissionStatus { state: "granted" }

const desc2 = { name: "read", path: "/foo/bar" } as const;
console.log(await Deno.permissions.query(desc2));
// PermissionStatus { state: "granted" }

const desc3 = { name: "read", path: "/bar" } as const;
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.

Permission strength

The intuitive understanding behind the result of the second query in 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 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:

const desc1 = { name: "write" } as const;
// is stronger than
const desc2 = { name: "write", path: "/foo" } as const;

const desc3 = { name: "net", host: "127.0.0.1" } as const;
// is stronger than
const desc4 = { name: "net", host: "127.0.0.1:8000" } as const;

Request permissions

Request an ungranted permission from the user via CLI prompt.

// deno run --unstable main.ts

const desc1 = { name: "read", path: "/foo" } as const;
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" } as const;
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".

// deno run --unstable --allow-read=/foo main.ts

const desc = { name: "read", path: "/foo" } as const;
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?

// deno run --unstable --allow-read=/foo main.ts

const desc = { name: "read", path: "/foo/bar" } as const;
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:

[
  { name: "read", path: "/foo" },
  { name: "read", path: "/bar" },
];

Granting a runtime request for { name: "write", path: "/foo" } updates the set to:

[
  { 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.

// deno run --unstable --allow-read=/foo main.ts

const desc = { name: "read", path: "/foo/bar" } as const;
console.log(await Deno.permissions.revoke(desc)); // Insufficient.
// PermissionStatus { state: "granted" }

const strongDesc = { name: "read", path: "/foo" } as const;
await Deno.permissions.revoke(strongDesc); // Good.

console.log(await Deno.permissions.query(desc));
// PermissionStatus { state: "prompt" }