The permissions API

A Collection of Interesting Ideas,

This version:
https://patrickkettner.github.io/quostibel/permissions-api.html
Issue Tracking:
GitHub
Inline In Spec
Editor:
Patrick Kettner

Abstract

This specification defines the permissions API namespace, through which an extension inspects, requests, and relinquishes its own optional permissions at runtime.

1. The permissions API

The permissions API lets an extension check, request, and relinquish its own optional permissions and optional host permissions at runtime, instead of requiring every permission at install time.

An optional permission is a permission the extension may hold without it being granted at install: one listed in optional_permissions, or one listed in permissions that the user agent has withheld until the extension requests it.

A named permission required by the extension’s manifest, or optional and granted to the extension at runtime, is part of the extension’s granted permissions.

1.1. Types

1.1.1. The Permissions dictionary

A Permissions dictionary’s permissions member is a list of named API permission strings, excluding host permissions. Its origins member is a list of match patterns, validated using the same match pattern grammar used for the host_permissions manifest key.

Which strings are valid entries in permissions is implementation-defined: each browser recognizes its own set of permission names, and a name outside a given browser’s set is not a valid entry for that browser. An entry in permissions or origins that the user agent does not recognize is an error. No permission is granted or revoked, and the error is surfaced to the caller.

These sixteen permission names are recognized by Chrome, Firefox and Safari alike:

A name outside this list is recognized by at most two of the three, so it is not portable.

Should an unrecognized name be signalled by throwing synchronously or by rejecting the returned promise?

A synchronous throw escapes a .catch() attached to the returned promise, so browser.permissions.contains(bad).then(ok).catch(err) behaves differently across browsers.

dictionary Permissions {
  sequence<DOMString> permissions;
  sequence<DOMString> origins;
};

1.2. Methods

[Exposed=WebExtensions]
interface permissions {
  Promise<Permissions> getAll();
  Promise<boolean> contains(optional Permissions permissions);
  Promise<boolean> request(optional Permissions permissions);
  Promise<undefined> remove(optional Permissions permissions);

  readonly attribute PermissionsEvent onAdded;
  readonly attribute PermissionsEvent onRemoved;
};

1.2.1. getAll()

getAll() returns the extension’s complete current set of permissions and match patterns: its granted permissions and granted host permissions.

  1. Let result be a new Permissions.

  2. Set result["permissions"] to the extension’s granted permissions.

  3. Set result["origins"] to the extension’s granted host permissions.

  4. Resolve with result.

May getAll() return an all-hosts pattern that no single granted permission contains, when the granted patterns together cover all hosts? Safari does, returning *://*/*. Chrome and Firefox return only what was granted.

1.2.2. contains()

contains(permissions) checks whether the extension’s granted permissions and granted host permissions include every permission and origin named in permissions.

  1. For each named permission in permissions["permissions"]:

    1. If it is not in the extension’s granted permissions, resolve with false.

  2. For each match pattern in permissions["origins"]:

    1. If it is not subsumed by the extension’s granted host permissions, resolve with false.

  3. Resolve with true.

1.2.3. request()

request(permissions) asks the user to grant the extension some or all of permissions that are not already part of its granted permissions or granted host permissions.

  1. If this call is not happening within a user activation, reject with an error.

  2. If any named permission in permissions["permissions"] is not an optional permission of the extension, reject with an error.

  3. If any match pattern in permissions["origins"] is not subsumed by an optional host permission of the extension, reject with an error.

  4. If every named permission in permissions["permissions"] is in the extension’s granted permissions, and every match pattern in permissions["origins"] is subsumed by the extension’s granted host permissions, resolve with true.

  5. Prompt the user to approve the permissions and origins in permissions that are not already part of the extension’s granted permissions or granted host permissions. Skip this prompt, and grant those permissions and origins automatically, if they carry no additional user-facing warnings beyond what the extension is already granted.

  6. If the user does not approve the prompt, resolve with false.

  7. Grant the extension the requested permissions and origins not already in its granted permissions or granted host permissions. This fires onAdded for the newly granted set.

  8. Resolve with true.

What does request() reject with, for a missing user activation, an unlisted permission, or an unlisted origin? Chrome rejects with an Error: one fixed message for a missing user activation, and a second, shared fixed message for an unlisted permission or an unlisted origin. Firefox rejects with an ExtensionError. Safari reports a callback/promise error with no named type.

What happens when no window is available to show the prompt in, separate from the user-activation requirement? Chrome rejects, distinctly from the no-activation rejection. Firefox has no window requirement and prompts through whatever surface is available. Safari has no window concept: the embedding application prompts, and the call resolves false if it never responds.

Chrome and Safari exclude the entire permissions namespace from content scripts. Firefox exposes request() alone to content scripts, keeping the rest of the namespace restricted to background and extension-page contexts.

A user agent may decline a request without prompting the user, for example where administrative policy has already denied the permission.

Does a request declined by policy reject, or resolve false? Chrome and Firefox reject. Safari resolves false without prompting.

1.2.4. remove()

remove(permissions) relinquishes some or all of the extension’s optional granted permissions and granted host permissions.

  1. If any named permission or match pattern in permissions is not an optional permission or optional host permission of the extension, reject with an error.

  2. If any named permission or match pattern in permissions is not declared anywhere in the extension’s manifest, reject with an error.

  3. Revoke each named permission in permissions["permissions"] that is in the extension’s granted permissions, and each match pattern in permissions["origins"] that is subsumed by the extension’s granted host permissions. Entries in permissions not granted are ignored rather than treated as an error. This fires onRemoved for the revoked set, if any.

  4. Resolve with undefined.

Why does remove() resolve with undefined here, when every shipping browser resolves true?

request() and contains() are not affected: both resolve a boolean that carries real information (whether the request was granted, whether the extension has the permission), so neither changes.

1.3. Events

Should the event-listener pattern (addListener(), removeListener(), hasListener()) used by every event in this specification be defined once, as a shared events.Event type, rather than duplicated as PermissionsEvent in each namespace that fires one?

PermissionsEvent below is a minimal local stand-in pending that decision.

callback PermissionsListener = undefined (optional Permissions changed);

interface PermissionsEvent {
  undefined addListener(PermissionsListener listener);
  undefined removeListener(PermissionsListener listener);
  boolean hasListener(PermissionsListener listener);
};

1.3.1. onAdded

The onAdded event fires with a Permissions dictionary listing the permissions and origins newly granted to the extension, whenever the extension’s granted permissions or granted host permissions increase. This includes grants made through request, and grants made through user agent UI outside of the permissions API, for example the user agent’s own controls for granting a withheld host permission.

Should onAdded/onRemoved fire for a permission or host-permission change driven entirely by administrative or managed policy?

1.3.2. onRemoved

The onRemoved event fires with a Permissions dictionary listing the permissions and origins no longer granted to the extension, whenever the extension’s granted permissions or granted host permissions decrease, whether through remove or through user agent UI.

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSP-EMBEDDED-ENFORCEMENT]
Mike West; Antonio Sartori. Content Security Policy: Embedded Enforcement. URL: https://w3c.github.io/webappsec-cspee/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

IDL Index

dictionary Permissions {
  sequence<DOMString> permissions;
  sequence<DOMString> origins;
};

[Exposed=WebExtensions]
interface permissions {
  Promise<Permissions> getAll();
  Promise<boolean> contains(optional Permissions permissions);
  Promise<boolean> request(optional Permissions permissions);
  Promise<undefined> remove(optional Permissions permissions);

  readonly attribute PermissionsEvent onAdded;
  readonly attribute PermissionsEvent onRemoved;
};

callback PermissionsListener = undefined (optional Permissions changed);

interface PermissionsEvent {
  undefined addListener(PermissionsListener listener);
  undefined removeListener(PermissionsListener listener);
  boolean hasListener(PermissionsListener listener);
};

Issues Index

Should an unrecognized name be signalled by throwing synchronously or by rejecting the returned promise?

A synchronous throw escapes a .catch() attached to the returned promise, so browser.permissions.contains(bad).then(ok).catch(err) behaves differently across browsers.

↵
May getAll() return an all-hosts pattern that no single granted permission contains, when the granted patterns together cover all hosts? Safari does, returning *://*/*. Chrome and Firefox return only what was granted. ↵
What does request() reject with, for a missing user activation, an unlisted permission, or an unlisted origin? Chrome rejects with an Error: one fixed message for a missing user activation, and a second, shared fixed message for an unlisted permission or an unlisted origin. Firefox rejects with an ExtensionError. Safari reports a callback/promise error with no named type. ↵
What happens when no window is available to show the prompt in, separate from the user-activation requirement? Chrome rejects, distinctly from the no-activation rejection. Firefox has no window requirement and prompts through whatever surface is available. Safari has no window concept: the embedding application prompts, and the call resolves false if it never responds. ↵
Does a request declined by policy reject, or resolve false? Chrome and Firefox reject. Safari resolves false without prompting. ↵
Why does remove() resolve with undefined here, when every shipping browser resolves true?

request() and contains() are not affected: both resolve a boolean that carries real information (whether the request was granted, whether the extension has the permission), so neither changes.

↵
Should the event-listener pattern (addListener(), removeListener(), hasListener()) used by every event in this specification be defined once, as a shared events.Event type, rather than duplicated as PermissionsEvent in each namespace that fires one? ↵
Should onAdded/onRemoved fire for a permission or host-permission change driven entirely by administrative or managed policy? ↵