Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
const COLLECTION_NAME = "remote-permissions";
/**
* Allowlist of permission types and values allowed to be set through remote
* settings. In this map, the key is the permission type, while the value is an
* array of allowed permission values/capabilities allowed to be set. Possible
* values for most permissions are:
*
* - Ci.nsIPermissionManager.ALLOW_ACTION
* - Ci.nsIPermissionManager.DENY_ACTION
* - Ci.nsIPermissionManager.PROMPT_ACTION
* - "*" (Allows all values)
*
* Permission types with custom permission values (like
* https-only-load-insecure) may include different values. Only change this
* value with a review from #permissions-reviewers.
*/
const ALLOWED_PERMISSION_VALUES = {
"https-only-load-insecure": [
Ci.nsIHttpsOnlyModePermission.HTTPSFIRST_LOAD_INSECURE_ALLOW,
],
};
/**
* See nsIRemotePermissionService.idl
*/
export class RemotePermissionService {
classId = Components.ID("{a4b1b3b1-b68a-4129-aa2f-eb086162a8c7}");
QueryInterface = ChromeUtils.generateQI(["nsIRemotePermissionService"]);
#rs = RemoteSettings(COLLECTION_NAME);
#onSyncCallback = null;
#initialized = Promise.withResolvers();
#allowedPermissionValues = ALLOWED_PERMISSION_VALUES;
/**
* Asynchonously import all default permissions from remote settings into the
* permission manager. Also, if not already done, set up remote settings event
* listener to keep remote permissions in sync.
*/
async init() {
try {
if (Services.startup.shuttingDown) {
return;
}
if (
!Services.prefs.getBoolPref("permissions.manager.remote.enabled", false)
) {
throw Error(
"Tried to initialize remote permission service despite being disabled by pref"
);
}
let remotePermissions = await this.#rs.get();
for (const permission of remotePermissions) {
this.#addDefaultPermission(permission);
}
// Init could be called multiple times if the permission manager is
// reinitializing itself due to "testonly-reload-permissions-from-disk"
// being emitted. In that case, we don't shouldn't set up the RS listener
// again. We may also land in that situtation when "profile-do-change" is
// emitted.
if (!this.#onSyncCallback) {
this.#onSyncCallback = this.#onSync.bind(this);
this.#rs.on("sync", this.#onSyncCallback);
}
this.#initialized.resolve();
} catch (e) {
this.#initialized.reject(e);
throw e;
}
}
get isInitialized() {
return this.#initialized.promise;
}
get testAllowedPermissionValues() {
return this.#allowedPermissionValues;
}
set testAllowedPermissionValues(allowedPermissionValues) {
Cu.crashIfNotInAutomation();
this.#allowedPermissionValues = allowedPermissionValues;
}
// eslint-disable-next-line jsdoc/require-param
/**
* Callback for the "sync" event from remote settings. This function will
* receive the created, updated and deleted permissions from remote settings,
* and will update the permission manager accordingly.
*/
#onSync({ data: { created = [], updated = [], deleted = [] } }) {
const toBeDeletedPermissions = [
// Delete permissions that got deleted in remote settings.
...deleted,
// If an existing entry got updated in remote settings, but the origin or
// type changed, we can not just update it, as permissions are identified
// by origin and type in the permission manager. Instead, we need to
// remove the old permission and add a new one.
...updated
.filter(
({
old: { origin: oldOrigin, type: oldType },
new: { origin: newOrigin, type: newType },
}) => oldOrigin != newOrigin || oldType != newType
)
.map(({ old }) => old),
];
const toBeAddedPermissions = [
// Add newly created permissions.
...created,
// "Add" permissions updated in remote settings (the permission manager
// will automatically update the existing default permission instead of
// creating a new one if the permission origin and type match).
...updated.map(({ new: newPermission }) => newPermission),
// Delete permissions by "adding" them with value UNKNOWN_ACTION.
...toBeDeletedPermissions.map(({ origin, type }) => ({
origin,
type,
capability: Ci.nsIPermissionManager.UNKNOWN_ACTION,
})),
];
for (const permission of toBeAddedPermissions) {
this.#addDefaultPermission(permission);
}
}
/**
* Check if a permission type and value is allowed to be set through remote
* settings, based on the ALLOWED_PERMISSION_VALUES allowlist.
*
* @param {string} type Permission type to check
* @param {string} capability Permission capability to check
* @returns {boolean}
*/
#isAllowed(type, capability) {
if (!this.#allowedPermissionValues[type]) {
if (this.#allowedPermissionValues["*"]) {
this.#allowedPermissionValues[type] =
this.#allowedPermissionValues["*"];
} else {
return false;
}
}
return (
this.#allowedPermissionValues[type].includes("*") ||
this.#allowedPermissionValues[type].includes(capability) ||
capability === Ci.nsIPermissionManager.UNKNOWN_ACTION
);
}
/**
* Add a default permission to the permission manager.
*
* @param {object} permission The permission to add
* @param {string} permission.origin Origin string of the permission
* @param {string} permission.type Type of the permission
* @param {number} permission.capability Capability of the permission
*/
#addDefaultPermission({ origin, type, capability }) {
if (!this.#isAllowed(type, capability)) {
console.error(
`Remote Settings contain default permission of disallowed type '${type}' with value '${capability}' for origin '${origin}', skipping import`
);
return;
}
try {
let principal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI(origin),
{}
);
Services.perms.addDefaultFromPrincipal(principal, type, capability);
} catch (e) {
console.error(e);
}
}
}