Bug 1542403 Add privileged activity logging api r=rpl,zombie

Differential Revision: https://phabricator.services.mozilla.com/D34440

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Shane Caraveo 2019-08-16 23:00:56 +00:00
parent 3a4666603f
commit 4c24a7f0ac
11 changed files with 450 additions and 0 deletions

View File

@ -147,6 +147,7 @@ const CHILD_SHUTDOWN_TIMEOUT_MS = 8000;
// Permissions that are only available to privileged extensions.
const PRIVILEGED_PERMS = new Set([
"activityLog",
"mozillaAddons",
"geckoViewAddons",
"telemetry",

View File

@ -82,6 +82,14 @@
["extension"]
]
},
"activityLog": {
"url": "chrome://extensions/content/parent/ext-activityLog.js",
"schema": "chrome://extensions/content/schemas/activity_log.json",
"scopes": ["addon_parent"],
"paths": [
["activityLog"]
]
},
"i18n": {
"url": "chrome://extensions/content/parent/ext-i18n.js",
"schema": "chrome://extensions/content/schemas/i18n.json",

View File

@ -7,6 +7,7 @@ toolkit.jar:
content/extensions/dummy.xul
content/extensions/ext-browser-content.js
content/extensions/ext-toolkit.json
content/extensions/parent/ext-activityLog.js (parent/ext-activityLog.js)
content/extensions/parent/ext-alarms.js (parent/ext-alarms.js)
content/extensions/parent/ext-backgroundPage.js (parent/ext-backgroundPage.js)
content/extensions/parent/ext-browserSettings.js (parent/ext-browserSettings.js)

View File

@ -0,0 +1,38 @@
/* 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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
ChromeUtils.defineModuleGetter(
this,
"ExtensionCommon",
"resource://gre/modules/ExtensionCommon.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ExtensionActivityLog",
"resource://gre/modules/ExtensionActivityLog.jsm"
);
this.activityLog = class extends ExtensionAPI {
getAPI(context) {
return {
activityLog: {
onExtensionActivity: new ExtensionCommon.EventManager({
context,
name: "activityLog.onExtensionActivity",
register: (fire, id) => {
function handler(details) {
fire.async(details);
}
ExtensionActivityLog.addListener(id, handler);
return () => {
ExtensionActivityLog.removeListener(id, handler);
};
},
}).api(),
},
};
}
};

View File

@ -0,0 +1,87 @@
[
{
"namespace": "manifest",
"types": [{
"$extend": "Permission",
"choices": [{
"type": "string",
"enum": [
"activityLog"
]
}]
}]
},
{
"namespace": "activityLog",
"description": "Monitor extension activity",
"permissions": ["activityLog"],
"events": [
{
"name": "onExtensionActivity",
"description": "Receives an activityItem for each logging event.",
"type": "function",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"timeStamp": {
"$ref": "extensionTypes.Date",
"description": "The date string when this call is triggered."
},
"type": {
"type": "string",
"enum": ["api_call", "api_event", "content_script", "user_script"],
"description": "The type of log entry. api_call is a function call made by the extension and api_event is an event callback to the extension. content_script is logged when a content script is injected."
},
"viewType": {
"type": "string",
"optional": true,
"enum": ["background", "popup", "sidebar", "tab", "devtools_page", "devtools_panel"],
"description": "The type of view where the activity occurred. Content scripts will not have a viewType."
},
"name": {
"type": "string",
"description": "The name of the api call or event, or the script url if this is a content or user script event."
},
"data": {
"type": "object",
"properties": {
"args": {
"type": "array",
"optional": true,
"items": {
"type": "any"
},
"description": "A list of arguments passed to the call."
},
"result": {
"type": "object",
"optional": true,
"description": "The result of the call."
},
"tabId": {
"type": "integer",
"optional": true,
"description": "The tab associated with this event if it is a tab or content script."
},
"url": {
"type": "string",
"optional": true,
"description": "If the type is content_script, this is the url of the script that was injected."
}
}
}
}
}
],
"extraParameters": [
{
"name": "id",
"type": "string"
}
]
}
]
}
]

View File

@ -4,6 +4,7 @@
toolkit.jar:
% content extensions %content/extensions/
content/extensions/schemas/activity_log.json
content/extensions/schemas/alarms.json
content/extensions/schemas/browser_settings.json
#ifndef ANDROID

View File

@ -61,6 +61,7 @@ prefs =
browser.chrome.guess_favicon=true
skip-if = toolkit == 'android' && !is_fennec
[test_ext_activityLog.html]
[test_ext_async_clipboard.html]
skip-if = fission || toolkit == 'android' # near-permafail after landing bug 1270059: Bug 1523131
[test_ext_background_canvas.html]

View File

@ -0,0 +1,290 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebExtension activityLog test</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="text/javascript">
"use strict";
add_task(async function test_api() {
let URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_sample.html";
let extension = ExtensionTestUtils.loadExtension({
manifest: {
applications: { gecko: { id: "watched@tests.mozilla.org" } },
permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
content_scripts: [
{
matches: ["http://mochi.test/*/file_sample.html"],
js: ["content_script.js"],
run_at: "document_idle",
},
],
},
files: {
"content_script.js": () => {
browser.test.sendMessage("content_script");
},
"registered_script.js": () => {
browser.test.sendMessage("registered_script");
},
},
async background() {
async function runTest() {
// Test activity for a child function call.
browser.test.assertEq(
undefined,
browser.activityLog,
"activityLog requires permission"
);
// Test a child event manager.
browser.test.onMessage.addListener(async msg => {
});
// Test a parent event manager.
browser.webRequest.onBeforeRequest.addListener(
details => {
return { cancel: false };
},
{ urls: ["http://mochi.test/*/file_sample.html"] },
["blocking"]
);
// A manifest based content script is already
// registered, we do a dynamic registration here.
await browser.contentScripts.register({
js: [{ file: "registered_script.js" }],
matches: ["http://mochi.test/*/file_sample.html"],
runAt: "document_start",
});
browser.test.sendMessage("ready");
}
browser.test.onMessage.addListener(msg => {
// Logging has started here so this listener is logged, but the
// call adding it was not. We do an additional onMessage.addListener
// call in the test function to validate child based event managers.
if (msg == "runtest") {
browser.test.assertTrue(true, msg);
runTest();
}
});
browser.test.sendMessage("url", browser.extension.getURL(""));
},
});
async function backgroundScript(expectedUrl, extensionUrl) {
let expecting = [
// Test child-only api_call.
{
type: "api_call",
name: "test.assertTrue",
data: { args: [true, "runtest"] },
},
// Test child-only api_call.
{
type: "api_call",
name: "test.assertEq",
data: {
args: [null, null, "activityLog requires permission"],
},
},
// Test child addListener calls.
{
type: "api_call",
name: "test.onMessage.addListener",
data: {
args: [],
},
},
// Test parent addListener calls.
{
type: "api_call",
name: "webRequest.onBeforeRequest.addListener",
data: {
args: [
{
incognito: null,
tabId: null,
types: null,
urls: ["http://mochi.test/*/file_sample.html"],
windowId: null,
},
["blocking"],
],
},
},
// Test an api that makes use of callParentAsyncFunction.
{
type: "api_call",
name: "contentScripts.register",
data: {
args: [
{
allFrames: null,
css: null,
excludeGlobs: null,
excludeMatches: null,
includeGlobs: null,
js: [
{
file: `${extensionUrl}registered_script.js`,
},
],
matchAboutBlank: null,
matches: ["http://mochi.test/*/file_sample.html"],
runAt: "document_start",
},
],
},
},
// Test child api_event calls.
{
type: "api_event",
name: "test.onMessage",
data: { args: ["runtest"] },
},
{
type: "api_call",
name: "test.sendMessage",
data: { args: ["ready"] },
},
// Test parent api_event calls.
{
type: "api_event",
name: "webRequest.onBeforeRequest",
data: {
args: [
{
url: expectedUrl,
method: "GET",
type: "main_frame",
frameId: 0,
parentFrameId: -1,
ip: null,
frameAncestors: [],
incognito: false,
},
],
result: {
cancel: false,
},
},
},
// Test manifest based content script.
{
type: "content_script",
name: "content_script.js",
data: { url: expectedUrl, tabId: 1 },
},
// registered script test
{
type: "content_script",
name: `${extensionUrl}registered_script.js`,
data: { url: expectedUrl, tabId: 1 },
},
{
type: "api_call",
name: "test.sendMessage",
data: { args: ["registered_script"], tabId: 1 },
},
{
type: "api_call",
name: "test.sendMessage",
data: { args: ["content_script"], tabId: 1 },
},
];
browser.test.assertTrue(browser.activityLog, "activityLog is privileged");
let tab;
browser.activityLog.onExtensionActivity.addListener(async details => {
browser.test.log(`onExtensionActivity ${JSON.stringify(details)}`);
let test = expecting.shift();
if (!test) {
browser.test.notifyFail(`no test for ${details.name}`);
}
delete details.timeStamp;
// On multiple runs, tabId will be different. Get the current tabId and
// use that.
if (test.data.tabId !== undefined) {
test.data.tabId = tab.id;
}
// hack for webRequest test
if (details.name === "webRequest.onBeforeRequest") {
// Remove items that may be variable, the important
// aspect is that we generally get the activity
// logging we expect.
delete details.data.args[0].requestId;
delete details.data.args[0].tabId;
delete details.data.args[0].originUrl;
delete details.data.args[0].timeStamp;
delete details.data.args[0].proxyInfo;
}
browser.test.assertEq(test.type, details.type, "type matches");
if (test.type == "content_script") {
browser.test.assertTrue(
details.name.includes(test.name),
"content script name matches"
);
} else {
browser.test.assertEq(test.name, details.name, "name matches");
}
browser.test.assertEq(
JSON.stringify(test.data),
JSON.stringify(details.data),
"message matches"
);
if (expecting.length == 0) {
await browser.tabs.remove(tab.id);
browser.test.notifyPass("activity");
}
}, "watched@tests.mozilla.org");
browser.test.onMessage.addListener(async msg => {
if (msg === "opentab") {
tab = await browser.tabs.create({url: expectedUrl});
}
});
}
await extension.startup();
let extensionUrl = await extension.awaitMessage("url");
let logger = ExtensionTestUtils.loadExtension({
isPrivileged: true,
manifest: {
applications: { gecko: { id: "watcher@tests.mozilla.org" } },
permissions: ["activityLog"],
},
background: `(${backgroundScript})("${URL}", "${extensionUrl}")`,
});
await logger.startup();
extension.sendMessage("runtest");
await extension.awaitMessage("ready");
logger.sendMessage("opentab");
await Promise.all([
extension.awaitMessage("content_script"),
extension.awaitMessage("registered_script"),
logger.awaitFinish("activity"),
]);
await extension.unload();
await logger.unload();
});
</script>
</body>
</html>

View File

@ -0,0 +1,21 @@
"use strict";
add_task(async function test_api_restricted() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
applications: {
gecko: { id: "activityLog-permission@tests.mozilla.org" },
},
permissions: ["activityLog"],
},
async background() {
browser.test.assertEq(
undefined,
browser.activityLog,
"activityLog is privileged"
);
},
});
await extension.startup();
await extension.unload();
});

View File

@ -566,6 +566,7 @@ const GRANTED_WITHOUT_USER_PROMPT = [
"contextMenus",
"contextualIdentities",
"cookies",
"activityLog",
"geckoProfiler",
"identity",
"idle",

View File

@ -1,5 +1,6 @@
[test_ext_MessageManagerProxy.js]
skip-if = os == 'android' # Bug 1545439
[test_ext_activityLog.js]
[test_ext_alarms.js]
[test_ext_alarms_does_not_fire.js]
[test_ext_alarms_periodic.js]