Permission APIs
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;
⚠️ See
PermissionDescriptor
in API reference for more details.
⚠️ In 1.30 and onwards, synchronous API counterparts (ex.
Deno.permissions.querySync
) exist for all the APIs described below.
Query permissions
Check, by descriptor, if a permission is granted or not.
// deno run --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:
- If
desc1
queries to{ state: "granted" }
then so mustdesc2
. - If
desc2
queries to{ state: "denied" }
then so mustdesc1
.
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 main.ts
const desc1 = { name: "read", path: "/foo" } as const;
const status1 = await Deno.permissions.request(desc1);
// ⚠️ Deno requests read access to "/foo". Grant? [y/n (y = yes allow, n = no deny)] y
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? [y/n (y = yes allow, n = no deny)] n
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 --allow-read=/foo main.ts
const desc = { name: "read", path: "/foo" } as const;
console.log(await Deno.permissions.revoke(desc));
// PermissionStatus { state: "prompt" }
What happens when you try to revoke a permission which is partial to one granted on the CLI?
// deno run --allow-read=/foo main.ts
const desc = { name: "read", path: "/foo/bar" } as const;
console.log(await Deno.permissions.revoke(desc));
// PermissionStatus { state: "prompt" }
const cliDesc = { name: "read", path: "/foo" } as const;
console.log(await Deno.permissions.revoke(cliDesc));
// PermissionStatus { state: "prompt" }
The CLI-granted permission, which implies the revoked permission, was also 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 is stronger than the argument permission descriptor.
Deno does not allow "fragmented" permission states, where some strong permission
is granted with exclusions of weak permissions implied by it. Such a system
would prove increasingly complex and unpredictable as you factor in a wider
variety of use cases and the "denied"
state. This is a calculated trade-off of
granularity for security.