feat(permissions): add "--deny-*" flags (#19070)

This commit adds new "--deny-*" permission flags. These are complimentary to
"--allow-*" flags.

These flags can be used to restrict access to certain resources, even if they
were granted using "--allow-*" flags or the "--allow-all" ("-A") flag.

Eg. specifying "--allow-read --deny-read" will result in a permission error,
while "--allow-read --deny-read=/etc" will allow read access to all FS but the
"/etc" directory.

Runtime permissions APIs ("Deno.permissions") were adjusted as well, mainly
by adding, a new "PermissionStatus.partial" field. This field denotes that
while permission might be granted to requested resource, it's only partial (ie.
a "--deny-*" flag was specified that excludes some of the requested resources).
Eg. specifying "--allow-read=foo/ --deny-read=foo/bar" and then querying for
permissions like "Deno.permissions.query({ name: "read", path: "foo/" })"
will return "PermissionStatus { state: "granted", onchange: null, partial: true }",
denoting that some of the subpaths don't have read access.

Closes #18804.

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
Co-authored-by: Nayeem Rahman <nayeemrmn99@gmail.com>
This commit is contained in:
Asher Gomez 2023-08-03 21:19:19 +10:00 committed by GitHub
parent db287e216d
commit 6fb7e8d93b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1805 additions and 1456 deletions

View file

@ -30,6 +30,7 @@ const illegalConstructorKey = Symbol("illegalConstructorKey");
* @typedef StatusCacheValue
* @property {PermissionState} state
* @property {PermissionStatus} status
* @property {boolean} partial
*/
/** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "sys" | "run" | "ffi" | "hrtime">} */
@ -69,27 +70,32 @@ function opRequest(desc) {
}
class PermissionStatus extends EventTarget {
/** @type {{ state: Deno.PermissionState }} */
#state;
/** @type {{ state: Deno.PermissionState, partial: boolean }} */
#status;
/** @type {((this: PermissionStatus, event: Event) => any) | null} */
onchange = null;
/** @returns {Deno.PermissionState} */
get state() {
return this.#state.state;
return this.#status.state;
}
/** @returns {boolean} */
get partial() {
return this.#status.partial;
}
/**
* @param {{ state: Deno.PermissionState }} state
* @param {{ state: Deno.PermissionState, partial: boolean }} status
* @param {unknown} key
*/
constructor(state = null, key = null) {
constructor(status = null, key = null) {
if (key != illegalConstructorKey) {
throw new TypeError("Illegal constructor.");
}
super();
this.#state = state;
this.#status = status;
}
/**
@ -106,9 +112,9 @@ class PermissionStatus extends EventTarget {
}
[SymbolFor("Deno.privateCustomInspect")](inspect) {
return `${this.constructor.name} ${
inspect({ state: this.state, onchange: this.onchange })
}`;
const object = { state: this.state, onchange: this.onchange };
if (this.partial) object.partial = this.partial;
return `${this.constructor.name} ${inspect(object)}`;
}
}
@ -117,10 +123,10 @@ const statusCache = new SafeMap();
/**
* @param {Deno.PermissionDescriptor} desc
* @param {Deno.PermissionState} state
* @param {{ state: Deno.PermissionState, partial: boolean }} rawStatus
* @returns {PermissionStatus}
*/
function cache(desc, state) {
function cache(desc, rawStatus) {
let { name: key } = desc;
if (
(desc.name === "read" || desc.name === "write" || desc.name === "ffi") &&
@ -139,18 +145,24 @@ function cache(desc, state) {
key += "$";
}
if (MapPrototypeHas(statusCache, key)) {
const status = MapPrototypeGet(statusCache, key);
if (status.state !== state) {
status.state = state;
status.status.dispatchEvent(new Event("change", { cancelable: false }));
const cachedObj = MapPrototypeGet(statusCache, key);
if (
cachedObj.state !== rawStatus.state ||
cachedObj.partial !== rawStatus.partial
) {
cachedObj.state = rawStatus.state;
cachedObj.partial = rawStatus.partial;
cachedObj.status.dispatchEvent(
new Event("change", { cancelable: false }),
);
}
return status.status;
return cachedObj.status;
}
/** @type {{ state: Deno.PermissionState; status?: PermissionStatus }} */
const status = { state };
status.status = new PermissionStatus(status, illegalConstructorKey);
MapPrototypeSet(statusCache, key, status);
return status.status;
/** @type {{ state: Deno.PermissionState, partial: boolean, status?: PermissionStatus }} */
const obj = rawStatus;
obj.status = new PermissionStatus(obj, illegalConstructorKey);
MapPrototypeSet(statusCache, key, obj);
return obj.status;
}
/**
@ -200,8 +212,8 @@ class Permissions {
formDescriptor(desc);
const state = opQuery(desc);
return cache(desc, state);
const status = opQuery(desc);
return cache(desc, status);
}
revoke(desc) {
@ -221,8 +233,8 @@ class Permissions {
formDescriptor(desc);
const state = opRevoke(desc);
return cache(desc, state);
const status = opRevoke(desc);
return cache(desc, status);
}
request(desc) {
@ -242,8 +254,8 @@ class Permissions {
formDescriptor(desc);
const state = opRequest(desc);
return cache(desc, state);
const status = opRequest(desc);
return cache(desc, status);
}
}