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

feat(runtime): stabilise permissions and add event target capabilities (#9573)

This commit is contained in:
Kitson Kelly 2021-02-25 14:33:09 +11:00 committed by GitHub
parent 90e4c5dcde
commit 097e9c44f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 333 additions and 176 deletions

View file

@ -19,21 +19,10 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[
"DiagnosticCategory", "DiagnosticCategory",
"DiagnosticItem", "DiagnosticItem",
"DiagnosticMessageChain", "DiagnosticMessageChain",
"EnvPermissionDescriptor",
"HrtimePermissionDescriptor",
"HttpClient", "HttpClient",
"LinuxSignal", "LinuxSignal",
"Location", "Location",
"MacOSSignal", "MacOSSignal",
"NetPermissionDescriptor",
"PermissionDescriptor",
"PermissionName",
"PermissionState",
"PermissionStatus",
"Permissions",
"PluginPermissionDescriptor",
"ReadPermissionDescriptor",
"RunPermissionDescriptor",
"Signal", "Signal",
"SignalStream", "SignalStream",
"StartTlsOptions", "StartTlsOptions",
@ -41,7 +30,6 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[
"TranspileOnlyResult", "TranspileOnlyResult",
"UnixConnectOptions", "UnixConnectOptions",
"UnixListenOptions", "UnixListenOptions",
"WritePermissionDescriptor",
"applySourceMap", "applySourceMap",
"connect", "connect",
"consoleSize", "consoleSize",
@ -64,7 +52,6 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[
"mainModule", "mainModule",
"openPlugin", "openPlugin",
"osRelease", "osRelease",
"permissions",
"ppid", "ppid",
"setRaw", "setRaw",
"shutdown", "shutdown",

View file

@ -2076,6 +2076,140 @@ declare namespace Deno {
*/ */
export function inspect(value: unknown, options?: InspectOptions): string; export function inspect(value: unknown, options?: InspectOptions): string;
/** The name of a "powerful feature" which needs permission. */
export type PermissionName =
| "run"
| "read"
| "write"
| "net"
| "env"
| "plugin"
| "hrtime";
/** The current status of the permission. */
export type PermissionState = "granted" | "denied" | "prompt";
export interface RunPermissionDescriptor {
name: "run";
}
export interface ReadPermissionDescriptor {
name: "read";
path?: string;
}
export interface WritePermissionDescriptor {
name: "write";
path?: string;
}
export interface NetPermissionDescriptor {
name: "net";
/** Optional host string of the form `"<hostname>[:<port>]"`. Examples:
*
* "github.com"
* "deno.land:8080"
*/
host?: string;
}
export interface EnvPermissionDescriptor {
name: "env";
}
export interface PluginPermissionDescriptor {
name: "plugin";
}
export interface HrtimePermissionDescriptor {
name: "hrtime";
}
/** Permission descriptors which define a permission and can be queried,
* requested, or revoked. */
export type PermissionDescriptor =
| RunPermissionDescriptor
| ReadPermissionDescriptor
| WritePermissionDescriptor
| NetPermissionDescriptor
| EnvPermissionDescriptor
| PluginPermissionDescriptor
| HrtimePermissionDescriptor;
export interface PermissionStatusEventMap {
"change": Event;
}
export class PermissionStatus extends EventTarget {
// deno-lint-ignore no-explicit-any
onchange: ((this: PermissionStatus, ev: Event) => any) | null;
readonly state: PermissionState;
addEventListener<K extends keyof PermissionStatusEventMap>(
type: K,
listener: (
this: PermissionStatus,
ev: PermissionStatusEventMap[K],
) => any,
options?: boolean | AddEventListenerOptions,
): void;
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener<K extends keyof PermissionStatusEventMap>(
type: K,
listener: (
this: PermissionStatus,
ev: PermissionStatusEventMap[K],
) => any,
options?: boolean | EventListenerOptions,
): void;
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions,
): void;
}
export class Permissions {
/** Resolves to the current status of a permission.
*
* ```ts
* const status = await Deno.permissions.query({ name: "read", path: "/etc" });
* if (status.state === "granted") {
* data = await Deno.readFile("/etc/passwd");
* }
* ```
*/
query(desc: PermissionDescriptor): Promise<PermissionStatus>;
/** Revokes a permission, and resolves to the state of the permission.
*
* ```ts
* const status = await Deno.permissions.revoke({ name: "run" });
* assert(status.state !== "granted")
* ```
*/
revoke(desc: PermissionDescriptor): Promise<PermissionStatus>;
/** Requests the permission, and resolves to the state of the permission.
*
* ```ts
* const status = await Deno.permissions.request({ name: "env" });
* if (status.state === "granted") {
* console.log("'env' permission is granted.");
* } else {
* console.log("'env' permission is denied.");
* }
* ```
*/
request(desc: PermissionDescriptor): Promise<PermissionStatus>;
}
/** Deno's permission management API. */
export const permissions: Permissions;
/** Build related information. */ /** Build related information. */
export const build: { export const build: {
/** The LLVM target triple */ /** The LLVM target triple */

View file

@ -1071,119 +1071,6 @@ declare namespace Deno {
* Requires `allow-run` permission. */ * Requires `allow-run` permission. */
export function kill(pid: number, signo: number): void; export function kill(pid: number, signo: number): void;
/** The name of a "powerful feature" which needs permission.
*
* See: https://w3c.github.io/permissions/#permission-registry
*
* Note that the definition of `PermissionName` in the above spec is swapped
* out for a set of Deno permissions which are not web-compatible. */
export type PermissionName =
| "run"
| "read"
| "write"
| "net"
| "env"
| "plugin"
| "hrtime";
/** The current status of the permission.
*
* See: https://w3c.github.io/permissions/#status-of-a-permission */
export type PermissionState = "granted" | "denied" | "prompt";
export interface RunPermissionDescriptor {
name: "run";
}
export interface ReadPermissionDescriptor {
name: "read";
path?: string;
}
export interface WritePermissionDescriptor {
name: "write";
path?: string;
}
export interface NetPermissionDescriptor {
name: "net";
/** Optional host string of the form `"<hostname>[:<port>]"`. Examples:
*
* "github.com"
* "deno.land:8080"
*/
host?: string;
}
export interface EnvPermissionDescriptor {
name: "env";
}
export interface PluginPermissionDescriptor {
name: "plugin";
}
export interface HrtimePermissionDescriptor {
name: "hrtime";
}
/** Permission descriptors which define a permission and can be queried,
* requested, or revoked.
*
* See: https://w3c.github.io/permissions/#permission-descriptor */
export type PermissionDescriptor =
| RunPermissionDescriptor
| ReadPermissionDescriptor
| WritePermissionDescriptor
| NetPermissionDescriptor
| EnvPermissionDescriptor
| PluginPermissionDescriptor
| HrtimePermissionDescriptor;
export class Permissions {
/** Resolves to the current status of a permission.
*
* ```ts
* const status = await Deno.permissions.query({ name: "read", path: "/etc" });
* if (status.state === "granted") {
* data = await Deno.readFile("/etc/passwd");
* }
* ```
*/
query(desc: PermissionDescriptor): Promise<PermissionStatus>;
/** Revokes a permission, and resolves to the state of the permission.
*
* const status = await Deno.permissions.revoke({ name: "run" });
* assert(status.state !== "granted")
*/
revoke(desc: PermissionDescriptor): Promise<PermissionStatus>;
/** Requests the permission, and resolves to the state of the permission.
*
* ```ts
* const status = await Deno.permissions.request({ name: "env" });
* if (status.state === "granted") {
* console.log("'env' permission is granted.");
* } else {
* console.log("'env' permission is denied.");
* }
* ```
*/
request(desc: PermissionDescriptor): Promise<PermissionStatus>;
}
/** **UNSTABLE**: Under consideration to move to `navigator.permissions` to
* match web API. It could look like `navigator.permissions.query({ name: Deno.symbols.read })`.
*/
export const permissions: Permissions;
/** see: https://w3c.github.io/permissions/#permissionstatus */
export class PermissionStatus {
state: PermissionState;
constructor();
}
/** **UNSTABLE**: New API, yet to be vetted. Additional consideration is still /** **UNSTABLE**: New API, yet to be vetted. Additional consideration is still
* necessary around the permissions required. * necessary around the permissions required.
* *

View file

@ -2226,7 +2226,7 @@ mod tests {
}, },
"end": { "end": {
"line": 0, "line": 0,
"character": 28 "character": 27
} }
} }
}), }),
@ -2255,9 +2255,9 @@ mod tests {
"contents": [ "contents": [
{ {
"language": "typescript", "language": "typescript",
"value": "const Deno.permissions: Deno.Permissions" "value": "function Deno.openPlugin(filename: string): number"
}, },
"**UNSTABLE**: Under consideration to move to `navigator.permissions` to\nmatch web API. It could look like `navigator.permissions.query({ name: Deno.symbols.read })`." "**UNSTABLE**: new API, yet to be vetted.\n\nOpen and initialize a plugin.\n\n```ts\nconst rid = Deno.openPlugin(\"./path/to/some/plugin.so\");\nconst opId = Deno.core.ops()[\"some_op\"];\nconst response = Deno.core.dispatch(opId, new Uint8Array([1,2,3,4]));\nconsole.log(`Response from plugin ${response}`);\n```\n\nRequires `allow-plugin` permission.\n\nThe plugin system is not stable and will change in the future, hence the\nlack of docs. For now take a look at the example\nhttps://github.com/denoland/deno/tree/master/test_plugin"
], ],
"range": { "range": {
"start": { "start": {
@ -2266,7 +2266,7 @@ mod tests {
}, },
"end": { "end": {
"line": 0, "line": 0,
"character": 28 "character": 27
} }
} }
}), }),

View file

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

View file

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

View file

@ -1,6 +1,6 @@
const status1 = await Deno.permissions.request({ name: "read" }); 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(status1);
const status2 = await Deno.permissions.query({ name: "read", path: "foo" });
console.log(status2); console.log(status2);
const status3 = await Deno.permissions.query({ name: "read", path: "bar" });
console.log(status3); console.log(status3);

View file

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

View file

@ -1,6 +1,6 @@
const status1 = await Deno.permissions.revoke({ name: "read", path: "foo" }); 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(status1);
const status2 = await Deno.permissions.query({ name: "read", path: "bar" });
console.log(status2); console.log(status2);
const status3 = await Deno.permissions.revoke({ name: "read", path: "bar" });
console.log(status3); console.log(status3);

View file

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

View file

@ -1,6 +1,6 @@
const status1 = await Deno.permissions.revoke({ name: "read" }); 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(status1);
const status2 = await Deno.permissions.query({ name: "read", path: "foo" });
console.log(status2); console.log(status2);
const status3 = await Deno.permissions.query({ name: "read", path: "bar" });
console.log(status3); console.log(status3);

View file

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

View file

@ -2677,7 +2677,7 @@ console.log("finish");
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn _061_permissions_request() { fn _061_permissions_request() {
let args = "run --unstable 061_permissions_request.ts"; let args = "run 061_permissions_request.ts";
let output = "061_permissions_request.ts.out"; let output = "061_permissions_request.ts.out";
let input = b"g\nd\n"; let input = b"g\nd\n";
@ -2687,7 +2687,7 @@ console.log("finish");
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn _062_permissions_request_global() { fn _062_permissions_request_global() {
let args = "run --unstable 062_permissions_request_global.ts"; let args = "run 062_permissions_request_global.ts";
let output = "062_permissions_request_global.ts.out"; let output = "062_permissions_request_global.ts.out";
let input = b"g\n"; let input = b"g\n";
@ -2695,13 +2695,12 @@ console.log("finish");
} }
itest!(_063_permissions_revoke { itest!(_063_permissions_revoke {
args: "run --unstable --allow-read=foo,bar 063_permissions_revoke.ts", args: "run --allow-read=foo,bar 063_permissions_revoke.ts",
output: "063_permissions_revoke.ts.out", output: "063_permissions_revoke.ts.out",
}); });
itest!(_064_permissions_revoke_global { itest!(_064_permissions_revoke_global {
args: args: "run --allow-read=foo,bar 064_permissions_revoke_global.ts",
"run --unstable --allow-read=foo,bar 064_permissions_revoke_global.ts",
output: "064_permissions_revoke_global.ts.out", output: "064_permissions_revoke_global.ts.out",
}); });

View file

@ -6,7 +6,7 @@
"uri": "file:///a/file.ts", "uri": "file:///a/file.ts",
"languageId": "typescript", "languageId": "typescript",
"version": 1, "version": 1,
"text": "console.log(Deno.permissions);\n" "text": "console.log(Deno.openPlugin);\n"
} }
} }
} }

View file

@ -1,5 +1,6 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import { import {
assert,
assertEquals, assertEquals,
assertThrows, assertThrows,
assertThrowsAsync, assertThrowsAsync,
@ -10,7 +11,7 @@ unitTest(async function permissionInvalidName(): Promise<void> {
await assertThrowsAsync(async () => { await assertThrowsAsync(async () => {
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
await Deno.permissions.query({ name: "foo" as any }); await Deno.permissions.query({ name: "foo" as any });
}, Error); }, TypeError);
}); });
unitTest(async function permissionNetInvalidHost(): Promise<void> { unitTest(async function permissionNetInvalidHost(): Promise<void> {
@ -19,8 +20,33 @@ unitTest(async function permissionNetInvalidHost(): Promise<void> {
}, URIError); }, URIError);
}); });
unitTest(async function permissionQueryReturnsEventTarget() {
const status = await Deno.permissions.query({ name: "hrtime" });
assert(["granted", "denied", "prompt"].includes(status.state));
let called = false;
status.addEventListener("change", () => {
called = true;
});
status.dispatchEvent(new Event("change"));
assert(called);
assert(status === (await Deno.permissions.query({ name: "hrtime" })));
});
unitTest(async function permissionQueryForReadReturnsSameStatus() {
const status1 = await Deno.permissions.query({
name: "read",
path: ".",
});
const status2 = await Deno.permissions.query({
name: "read",
path: ".",
});
assert(status1 === status2);
});
unitTest(function permissionsIllegalConstructor() { unitTest(function permissionsIllegalConstructor() {
assertThrows(() => new Deno.Permissions(), TypeError, "Illegal constructor."); assertThrows(() => new Deno.Permissions(), TypeError, "Illegal constructor.");
assertEquals(Deno.Permissions.length, 0);
}); });
unitTest(function permissionStatusIllegalConstructor() { unitTest(function permissionStatusIllegalConstructor() {

View file

@ -2,57 +2,178 @@
"use strict"; "use strict";
((window) => { ((window) => {
const core = window.Deno.core; const {
const { illegalConstructorKey } = window.__bootstrap.webUtil; Event,
EventTarget,
Deno: { core },
__bootstrap: { webUtil: { illegalConstructorKey } },
} = window;
/**
* @typedef StatusCacheValue
* @property {PermissionState} state
* @property {PermissionStatus} status
*/
/** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "run" | "plugin" | "hrtime">} */
const permissionNames = [
"read",
"write",
"net",
"env",
"run",
"plugin",
"hrtime",
];
/**
* @param {Deno.PermissionDescriptor} desc
* @returns {Deno.PermissionState}
*/
function opQuery(desc) { function opQuery(desc) {
return core.jsonOpSync("op_query_permission", desc).state; return core.jsonOpSync("op_query_permission", desc).state;
} }
/**
* @param {Deno.PermissionDescriptor} desc
* @returns {Deno.PermissionState}
*/
function opRevoke(desc) { function opRevoke(desc) {
return core.jsonOpSync("op_revoke_permission", desc).state; return core.jsonOpSync("op_revoke_permission", desc).state;
} }
/**
* @param {Deno.PermissionDescriptor} desc
* @returns {Deno.PermissionState}
*/
function opRequest(desc) { function opRequest(desc) {
return core.jsonOpSync("op_request_permission", desc).state; return core.jsonOpSync("op_request_permission", desc).state;
} }
class PermissionStatus { class PermissionStatus extends EventTarget {
/** @type {{ state: Deno.PermissionState }} */
#state;
/** @type {((this: PermissionStatus, event: Event) => any) | null} */
onchange = null;
/** @returns {Deno.PermissionState} */
get state() {
return this.#state.state;
}
/**
* @param {{ state: Deno.PermissionState }} state
* @param {unknown} key
*/
constructor(state = null, key = null) { constructor(state = null, key = null) {
if (key != illegalConstructorKey) { if (key != illegalConstructorKey) {
throw new TypeError("Illegal constructor."); throw new TypeError("Illegal constructor.");
} }
this.state = state; super();
this.#state = state;
} }
// TODO(kt3k): implement onchange handler
/**
* @param {Event} event
* @returns {boolean}
*/
dispatchEvent(event) {
let dispatched = super.dispatchEvent(event);
if (dispatched && this.onchange) {
this.onchange.call(this, event);
dispatched = !event.defaultPrevented;
}
return dispatched;
}
[Symbol.for("Deno.customInspect")](inspect) {
return `${this.constructor.name} ${
inspect({ state: this.state, onchange: this.onchange })
}`;
}
}
/** @type {Map<string, StatusCacheValue>} */
const statusCache = new Map();
/**
*
* @param {Deno.PermissionDescriptor} desc
* @param {Deno.PermissionState} state
* @returns {PermissionStatus}
*/
function cache(desc, state) {
let { name: key } = desc;
if ((desc.name === "read" || desc.name === "write") && "path" in desc) {
key += `-${desc.path}`;
} else if (desc.name === "net" && desc.host) {
key += `-${desc.host}`;
}
if (statusCache.has(key)) {
const status = statusCache.get(key);
if (status.state !== state) {
status.state = state;
status.status.dispatchEvent(new Event("change", { cancelable: false }));
}
return status.status;
}
/** @type {{ state: Deno.PermissionState; status?: PermissionStatus }} */
const status = { state };
status.status = new PermissionStatus(status, illegalConstructorKey);
statusCache.set(key, status);
return status.status;
}
/**
* @param {unknown} desc
* @returns {desc is Deno.PermissionDescriptor}
*/
function isValidDescriptor(desc) {
return desc && desc !== null && permissionNames.includes(desc.name);
} }
class Permissions { class Permissions {
constructor(key) { constructor(key = null) {
if (key != illegalConstructorKey) { if (key != illegalConstructorKey) {
throw new TypeError("Illegal constructor."); throw new TypeError("Illegal constructor.");
} }
} }
query(desc) { query(desc) {
if (!isValidDescriptor(desc)) {
return Promise.reject(
new TypeError(
`The provided value "${desc.name}" is not a valid permission name.`,
),
);
}
const state = opQuery(desc); const state = opQuery(desc);
return Promise.resolve( return Promise.resolve(cache(desc, state));
new PermissionStatus(state, illegalConstructorKey),
);
} }
revoke(desc) { revoke(desc) {
if (!isValidDescriptor(desc)) {
return Promise.reject(
new TypeError(
`The provided value "${desc.name}" is not a valid permission name.`,
),
);
}
const state = opRevoke(desc); const state = opRevoke(desc);
return Promise.resolve( return Promise.resolve(cache(desc, state));
new PermissionStatus(state, illegalConstructorKey),
);
} }
request(desc) { request(desc) {
if (!isValidDescriptor(desc)) {
return Promise.reject(
new TypeError(
`The provided value "${desc.name}" is not a valid permission name.`,
),
);
}
const state = opRequest(desc); const state = opRequest(desc);
return Promise.resolve( return Promise.resolve(cache(desc, state));
new PermissionStatus(state, illegalConstructorKey),
);
} }
} }

View file

@ -88,6 +88,9 @@
fsync: __bootstrap.fs.fsync, fsync: __bootstrap.fs.fsync,
fdatasyncSync: __bootstrap.fs.fdatasyncSync, fdatasyncSync: __bootstrap.fs.fdatasyncSync,
fdatasync: __bootstrap.fs.fdatasync, fdatasync: __bootstrap.fs.fdatasync,
permissions: __bootstrap.permissions.permissions,
Permissions: __bootstrap.permissions.Permissions,
PermissionStatus: __bootstrap.permissions.PermissionStatus,
}; };
__bootstrap.denoNsUnstable = { __bootstrap.denoNsUnstable = {
@ -96,9 +99,6 @@
Signal: __bootstrap.signals.Signal, Signal: __bootstrap.signals.Signal,
SignalStream: __bootstrap.signals.SignalStream, SignalStream: __bootstrap.signals.SignalStream,
emit: __bootstrap.compilerApi.emit, emit: __bootstrap.compilerApi.emit,
permissions: __bootstrap.permissions.permissions,
Permissions: __bootstrap.permissions.Permissions,
PermissionStatus: __bootstrap.permissions.PermissionStatus,
openPlugin: __bootstrap.plugins.openPlugin, openPlugin: __bootstrap.plugins.openPlugin,
kill: __bootstrap.process.kill, kill: __bootstrap.process.kill,
setRaw: __bootstrap.tty.setRaw, setRaw: __bootstrap.tty.setRaw,