RemotePermissionService.sys.mjs (6095B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { RemoteSettings } from "resource://services-settings/remote-settings.sys.mjs"; 6 7 const COLLECTION_NAME = "remote-permissions"; 8 9 /** 10 * Allowlist of permission types and values allowed to be set through remote 11 * settings. In this map, the key is the permission type, while the value is an 12 * array of allowed permission values/capabilities allowed to be set. Possible 13 * values for most permissions are: 14 * 15 * - Ci.nsIPermissionManager.ALLOW_ACTION 16 * - Ci.nsIPermissionManager.DENY_ACTION 17 * - Ci.nsIPermissionManager.PROMPT_ACTION 18 * - "*" (Allows all values) 19 * 20 * Permission types with custom permission values (like 21 * https-only-load-insecure) may include different values. Only change this 22 * value with a review from #permissions-reviewers. 23 */ 24 const ALLOWED_PERMISSION_VALUES = { 25 "https-only-load-insecure": [ 26 Ci.nsIHttpsOnlyModePermission.HTTPSFIRST_LOAD_INSECURE_ALLOW, 27 ], 28 localhost: ["*"], 29 "local-network": ["*"], 30 }; 31 32 /** 33 * See nsIRemotePermissionService.idl 34 */ 35 export class RemotePermissionService { 36 classId = Components.ID("{a4b1b3b1-b68a-4129-aa2f-eb086162a8c7}"); 37 QueryInterface = ChromeUtils.generateQI(["nsIRemotePermissionService"]); 38 39 #rs = RemoteSettings(COLLECTION_NAME); 40 #initialized = Promise.withResolvers(); 41 #allowedPermissionValues = ALLOWED_PERMISSION_VALUES; 42 43 constructor() { 44 this.init(); 45 } 46 47 /** 48 * Asynchonously import all default permissions from remote settings into the 49 * permission manager and set up remote settings event listener to keep 50 * remote permissions in sync. 51 */ 52 async init() { 53 try { 54 if (Services.startup.shuttingDown) { 55 return; 56 } 57 58 if ( 59 !Services.prefs.getBoolPref("permissions.manager.remote.enabled", false) 60 ) { 61 return; 62 } 63 64 let remotePermissions = await this.#rs.get(); 65 for (const permission of remotePermissions) { 66 this.#addDefaultPermission(permission); 67 } 68 69 this.#rs.on("sync", this.#onSync.bind(this)); 70 71 this.#initialized.resolve(); 72 } catch (e) { 73 this.#initialized.reject(e); 74 throw e; 75 } 76 } 77 78 get isInitialized() { 79 return this.#initialized.promise; 80 } 81 82 get testAllowedPermissionValues() { 83 return this.#allowedPermissionValues; 84 } 85 86 set testAllowedPermissionValues(allowedPermissionValues) { 87 Cu.crashIfNotInAutomation(); 88 this.#allowedPermissionValues = allowedPermissionValues; 89 } 90 91 // eslint-disable-next-line jsdoc/require-param 92 /** 93 * Callback for the "sync" event from remote settings. This function will 94 * receive the created, updated and deleted permissions from remote settings, 95 * and will update the permission manager accordingly. 96 */ 97 #onSync({ data: { created = [], updated = [], deleted = [] } }) { 98 const toBeDeletedPermissions = [ 99 // Delete permissions that got deleted in remote settings. 100 ...deleted, 101 // If an existing entry got updated in remote settings, but the origin or 102 // type changed, we can not just update it, as permissions are identified 103 // by origin and type in the permission manager. Instead, we need to 104 // remove the old permission and add a new one. 105 ...updated 106 .filter( 107 ({ 108 old: { origin: oldOrigin, type: oldType }, 109 new: { origin: newOrigin, type: newType }, 110 }) => oldOrigin != newOrigin || oldType != newType 111 ) 112 .map(({ old }) => old), 113 ]; 114 115 const toBeAddedPermissions = [ 116 // Add newly created permissions. 117 ...created, 118 // "Add" permissions updated in remote settings (the permission manager 119 // will automatically update the existing default permission instead of 120 // creating a new one if the permission origin and type match). 121 ...updated.map(({ new: newPermission }) => newPermission), 122 // Delete permissions by "adding" them with value UNKNOWN_ACTION. 123 ...toBeDeletedPermissions.map(({ origin, type }) => ({ 124 origin, 125 type, 126 capability: Ci.nsIPermissionManager.UNKNOWN_ACTION, 127 })), 128 ]; 129 130 for (const permission of toBeAddedPermissions) { 131 this.#addDefaultPermission(permission); 132 } 133 } 134 135 /** 136 * Check if a permission type and value is allowed to be set through remote 137 * settings, based on the ALLOWED_PERMISSION_VALUES allowlist. 138 * 139 * @param {string} type Permission type to check 140 * @param {string} capability Permission capability to check 141 * @returns {boolean} 142 */ 143 #isAllowed(type, capability) { 144 if (!this.#allowedPermissionValues[type]) { 145 if (this.#allowedPermissionValues["*"]) { 146 this.#allowedPermissionValues[type] = 147 this.#allowedPermissionValues["*"]; 148 } else { 149 return false; 150 } 151 } 152 153 return ( 154 this.#allowedPermissionValues[type].includes("*") || 155 this.#allowedPermissionValues[type].includes(capability) || 156 capability === Ci.nsIPermissionManager.UNKNOWN_ACTION 157 ); 158 } 159 160 /** 161 * Add a default permission to the permission manager. 162 * 163 * @param {object} permission The permission to add 164 * @param {string} permission.origin Origin string of the permission 165 * @param {string} permission.type Type of the permission 166 * @param {number} permission.capability Capability of the permission 167 */ 168 #addDefaultPermission({ origin, type, capability }) { 169 if (!this.#isAllowed(type, capability)) { 170 console.error( 171 `Remote Settings contain default permission of disallowed type '${type}' with value '${capability}' for origin '${origin}', skipping import` 172 ); 173 return; 174 } 175 176 try { 177 let principal = Services.scriptSecurityManager.createContentPrincipal( 178 Services.io.newURI(origin), 179 {} 180 ); 181 Services.perms.addDefaultFromPrincipal(principal, type, capability); 182 } catch (e) { 183 console.error(e); 184 } 185 } 186 }