mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Add monitoring only mode to frontend sandbox (#70688)
This commit is contained in:
parent
549e04a8f1
commit
72f6793344
@ -112,6 +112,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `extraThemes` | Enables extra themes |
|
||||
| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor |
|
||||
| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox |
|
||||
| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) |
|
||||
| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries |
|
||||
| `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore |
|
||||
| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries |
|
||||
|
@ -98,6 +98,7 @@ export interface FeatureToggles {
|
||||
extraThemes?: boolean;
|
||||
lokiPredefinedOperations?: boolean;
|
||||
pluginsFrontendSandbox?: boolean;
|
||||
frontendSandboxMonitorOnly?: boolean;
|
||||
sqlDatasourceDatabaseSelection?: boolean;
|
||||
cloudWatchLogsMonacoEditor?: boolean;
|
||||
exploreScrollableLogsContainer?: boolean;
|
||||
|
@ -545,6 +545,13 @@ var (
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "frontendSandboxMonitorOnly",
|
||||
Description: "Enables monitor only in the plugin frontend sandbox (if enabled)",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "sqlDatasourceDatabaseSelection",
|
||||
Description: "Enables previous SQL data source dataset dropdown behavior",
|
||||
|
@ -79,6 +79,7 @@ dataSourcePageHeader,preview,@grafana/enterprise-datasources,false,false,false,t
|
||||
extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,false,true
|
||||
lokiPredefinedOperations,experimental,@grafana/observability-logs,false,false,false,true
|
||||
pluginsFrontendSandbox,experimental,@grafana/plugins-platform-backend,false,false,false,true
|
||||
frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,false,false,true
|
||||
sqlDatasourceDatabaseSelection,preview,@grafana/grafana-bi-squad,false,false,false,true
|
||||
cloudWatchLogsMonacoEditor,experimental,@grafana/aws-plugins,false,false,false,true
|
||||
exploreScrollableLogsContainer,experimental,@grafana/observability-logs,false,false,false,true
|
||||
|
|
@ -327,6 +327,10 @@ const (
|
||||
// Enables the plugins frontend sandbox
|
||||
FlagPluginsFrontendSandbox = "pluginsFrontendSandbox"
|
||||
|
||||
// FlagFrontendSandboxMonitorOnly
|
||||
// Enables monitor only in the plugin frontend sandbox (if enabled)
|
||||
FlagFrontendSandboxMonitorOnly = "frontendSandboxMonitorOnly"
|
||||
|
||||
// FlagSqlDatasourceDatabaseSelection
|
||||
// Enables previous SQL data source dataset dropdown behavior
|
||||
FlagSqlDatasourceDatabaseSelection = "sqlDatasourceDatabaseSelection"
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { cloneDeep, isFunction } from 'lodash';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { forbiddenElements } from './constants';
|
||||
import { logWarning } from './utils';
|
||||
|
||||
/**
|
||||
* Distortions are near-membrane mechanisms to altert JS instrics and DOM APIs.
|
||||
@ -53,9 +56,11 @@ import { forbiddenElements } from './constants';
|
||||
* The code in this file defines that generalDistortionMap.
|
||||
*/
|
||||
|
||||
type DistortionMap = Map<unknown, (originalAttrOrMethod: unknown) => unknown>;
|
||||
type DistortionMap = Map<unknown, (originalAttrOrMethod: unknown, pluginId: string) => unknown>;
|
||||
const generalDistortionMap: DistortionMap = new Map();
|
||||
|
||||
const monitorOnly = Boolean(config.featureToggles.frontendSandboxMonitorOnly);
|
||||
|
||||
export function getGeneralSandboxDistortionMap() {
|
||||
if (generalDistortionMap.size === 0) {
|
||||
// initialize the distortion map
|
||||
@ -72,7 +77,15 @@ export function getGeneralSandboxDistortionMap() {
|
||||
return generalDistortionMap;
|
||||
}
|
||||
|
||||
function failToSet() {
|
||||
function failToSet(originalAttrOrMethod: unknown, pluginId: string) {
|
||||
logWarning(`Plugin ${pluginId} tried to set a sandboxed property`, {
|
||||
pluginId,
|
||||
attrOrMethod: String(originalAttrOrMethod),
|
||||
entity: 'window',
|
||||
});
|
||||
if (monitorOnly) {
|
||||
return originalAttrOrMethod;
|
||||
}
|
||||
return () => {
|
||||
throw new Error('Plugins are not allowed to set sandboxed properties');
|
||||
};
|
||||
@ -85,11 +98,22 @@ function distortIframeAttributes(distortions: DistortionMap) {
|
||||
for (const property of iframeHtmlForbiddenProperties) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, property);
|
||||
if (descriptor) {
|
||||
function fail() {
|
||||
function fail(originalAttrOrMethod: unknown, pluginId: string) {
|
||||
logWarning(`Plugin ${pluginId} tried to access iframe.${property}`, {
|
||||
pluginId,
|
||||
attrOrMethod: property,
|
||||
entity: 'iframe',
|
||||
});
|
||||
|
||||
if (monitorOnly) {
|
||||
return originalAttrOrMethod;
|
||||
}
|
||||
|
||||
return () => {
|
||||
throw new Error('iframe.' + property + ' is not allowed in sandboxed plugins');
|
||||
};
|
||||
}
|
||||
|
||||
if (descriptor.value) {
|
||||
distortions.set(descriptor.value, fail);
|
||||
}
|
||||
@ -107,20 +131,23 @@ function distortIframeAttributes(distortions: DistortionMap) {
|
||||
function distortConsole(distortions: DistortionMap) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(window, 'console');
|
||||
if (descriptor?.value) {
|
||||
function sandboxLog(...args: unknown[]) {
|
||||
console.log(`[plugin]`, ...args);
|
||||
}
|
||||
const sandboxConsole = {
|
||||
log: sandboxLog,
|
||||
warn: sandboxLog,
|
||||
error: sandboxLog,
|
||||
info: sandboxLog,
|
||||
debug: sandboxLog,
|
||||
table: sandboxLog,
|
||||
};
|
||||
function getSandboxConsole(originalAttrOrMethod: unknown, pluginId: string) {
|
||||
// we don't monitor the console because we expect a high volume of calls
|
||||
if (monitorOnly) {
|
||||
return originalAttrOrMethod;
|
||||
}
|
||||
|
||||
function getSandboxConsole() {
|
||||
return sandboxConsole;
|
||||
function sandboxLog(...args: unknown[]) {
|
||||
console.log(`[plugin ${pluginId}]`, ...args);
|
||||
}
|
||||
return {
|
||||
log: sandboxLog,
|
||||
warn: sandboxLog,
|
||||
error: sandboxLog,
|
||||
info: sandboxLog,
|
||||
debug: sandboxLog,
|
||||
table: sandboxLog,
|
||||
};
|
||||
}
|
||||
|
||||
distortions.set(descriptor.value, getSandboxConsole);
|
||||
@ -132,9 +159,19 @@ function distortConsole(distortions: DistortionMap) {
|
||||
|
||||
// set distortions to alert to always output to the console
|
||||
function distortAlert(distortions: DistortionMap) {
|
||||
function getAlertDistortion() {
|
||||
function getAlertDistortion(originalAttrOrMethod: unknown, pluginId: string) {
|
||||
logWarning(`Plugin ${pluginId} accessed window.alert`, {
|
||||
pluginId,
|
||||
attrOrMethod: 'alert',
|
||||
entity: 'window',
|
||||
});
|
||||
|
||||
if (monitorOnly) {
|
||||
return originalAttrOrMethod;
|
||||
}
|
||||
|
||||
return function (...args: unknown[]) {
|
||||
console.log(`[plugin]`, ...args);
|
||||
console.log(`[plugin ${pluginId}]`, ...args);
|
||||
};
|
||||
}
|
||||
const descriptor = Object.getOwnPropertyDescriptor(window, 'alert');
|
||||
@ -147,12 +184,22 @@ function distortAlert(distortions: DistortionMap) {
|
||||
}
|
||||
|
||||
function distortInnerHTML(distortions: DistortionMap) {
|
||||
function getInnerHTMLDistortion(originalMethod: unknown) {
|
||||
function getInnerHTMLDistortion(originalMethod: unknown, pluginId: string) {
|
||||
return function innerHTMLDistortion(this: HTMLElement, ...args: string[]) {
|
||||
for (const arg of args) {
|
||||
const lowerCase = arg?.toLowerCase() || '';
|
||||
for (const forbiddenElement of forbiddenElements) {
|
||||
if (lowerCase.includes('<' + forbiddenElement)) {
|
||||
logWarning(`Plugin ${pluginId} tried to set ${forbiddenElement} in innerHTML`, {
|
||||
pluginId,
|
||||
attrOrMethod: 'innerHTML',
|
||||
param: forbiddenElement,
|
||||
entity: 'HTMLElement',
|
||||
});
|
||||
|
||||
if (monitorOnly) {
|
||||
continue;
|
||||
}
|
||||
throw new Error('<' + forbiddenElement + '> is not allowed in sandboxed plugins');
|
||||
}
|
||||
}
|
||||
@ -181,10 +228,18 @@ function distortInnerHTML(distortions: DistortionMap) {
|
||||
}
|
||||
|
||||
function distortCreateElement(distortions: DistortionMap) {
|
||||
function getCreateElementDistortion(originalMethod: unknown) {
|
||||
function getCreateElementDistortion(originalMethod: unknown, pluginId: string) {
|
||||
return function createElementDistortion(this: HTMLElement, arg?: string, options?: unknown) {
|
||||
if (arg && forbiddenElements.includes(arg)) {
|
||||
return document.createDocumentFragment();
|
||||
logWarning(`Plugin ${pluginId} tried to create ${arg}`, {
|
||||
pluginId,
|
||||
attrOrMethod: 'createElement',
|
||||
param: arg,
|
||||
entity: 'document',
|
||||
});
|
||||
if (!monitorOnly) {
|
||||
return document.createDocumentFragment();
|
||||
}
|
||||
}
|
||||
if (isFunction(originalMethod)) {
|
||||
return originalMethod.apply(this, [arg, options]);
|
||||
@ -198,10 +253,20 @@ function distortCreateElement(distortions: DistortionMap) {
|
||||
}
|
||||
|
||||
function distortInsert(distortions: DistortionMap) {
|
||||
function getInsertDistortion(originalMethod: unknown) {
|
||||
function getInsertDistortion(originalMethod: unknown, pluginId: string) {
|
||||
return function insertChildDistortion(this: HTMLElement, node?: Node, ref?: Node) {
|
||||
if (node && forbiddenElements.includes(node.nodeName.toLowerCase())) {
|
||||
return document.createDocumentFragment();
|
||||
const nodeType = node?.nodeName?.toLowerCase() || '';
|
||||
|
||||
if (node && forbiddenElements.includes(nodeType)) {
|
||||
logWarning(`Plugin ${pluginId} tried to insert ${nodeType}`, {
|
||||
pluginId,
|
||||
attrOrMethod: 'insertChild',
|
||||
param: nodeType,
|
||||
entity: 'HTMLElement',
|
||||
});
|
||||
if (!monitorOnly) {
|
||||
return document.createDocumentFragment();
|
||||
}
|
||||
}
|
||||
if (isFunction(originalMethod)) {
|
||||
return originalMethod.call(this, node, ref);
|
||||
@ -209,10 +274,20 @@ function distortInsert(distortions: DistortionMap) {
|
||||
};
|
||||
}
|
||||
|
||||
function getinsertAdjacentElementDistortion(originalMethod: unknown) {
|
||||
function getinsertAdjacentElementDistortion(originalMethod: unknown, pluginId: string) {
|
||||
return function insertAdjacentElementDistortion(this: HTMLElement, position?: string, node?: Node) {
|
||||
if (node && forbiddenElements.includes(node.nodeName.toLowerCase())) {
|
||||
return document.createDocumentFragment();
|
||||
const nodeType = node?.nodeName?.toLowerCase() || '';
|
||||
if (node && forbiddenElements.includes(nodeType)) {
|
||||
logWarning(`Plugin ${pluginId} tried to insert ${nodeType}`, {
|
||||
pluginId,
|
||||
attrOrMethod: 'insertAdjacentElement',
|
||||
param: nodeType,
|
||||
entity: 'HTMLElement',
|
||||
});
|
||||
|
||||
if (!monitorOnly) {
|
||||
return document.createDocumentFragment();
|
||||
}
|
||||
}
|
||||
if (isFunction(originalMethod)) {
|
||||
return originalMethod.call(this, position, node);
|
||||
@ -240,9 +315,23 @@ function distortInsert(distortions: DistortionMap) {
|
||||
// set distortions to append elements to the document
|
||||
function distortAppend(distortions: DistortionMap) {
|
||||
// append accepts an array of nodes to append https://developer.mozilla.org/en-US/docs/Web/API/Node/append
|
||||
function getAppendDistortion(originalMethod: unknown) {
|
||||
function getAppendDistortion(originalMethod: unknown, pluginId: string) {
|
||||
return function appendDistortion(this: HTMLElement, ...args: Node[]) {
|
||||
const acceptedNodes = args?.filter((node) => !forbiddenElements.includes(node.nodeName.toLowerCase()));
|
||||
let acceptedNodes = args;
|
||||
const filteredAcceptedNodes = args?.filter((node) => !forbiddenElements.includes(node.nodeName.toLowerCase()));
|
||||
if (!monitorOnly) {
|
||||
acceptedNodes = filteredAcceptedNodes;
|
||||
}
|
||||
|
||||
if (acceptedNodes.length !== filteredAcceptedNodes.length) {
|
||||
logWarning(`Plugin ${pluginId} tried to append fobiddenElements`, {
|
||||
pluginId,
|
||||
attrOrMethod: 'append',
|
||||
param: args?.filter((node) => forbiddenElements.includes(node.nodeName.toLowerCase()))?.join(',') || '',
|
||||
entity: 'HTMLElement',
|
||||
});
|
||||
}
|
||||
|
||||
if (isFunction(originalMethod)) {
|
||||
originalMethod.apply(this, acceptedNodes);
|
||||
}
|
||||
@ -252,10 +341,20 @@ function distortAppend(distortions: DistortionMap) {
|
||||
}
|
||||
|
||||
// appendChild accepts a single node to add https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild
|
||||
function getAppendChildDistortion(originalMethod: unknown) {
|
||||
function getAppendChildDistortion(originalMethod: unknown, pluginId: string) {
|
||||
return function appendChildDistortion(this: HTMLElement, arg?: Node) {
|
||||
if (arg && forbiddenElements.includes(arg.nodeName.toLowerCase())) {
|
||||
return document.createDocumentFragment();
|
||||
const nodeType = arg?.nodeName?.toLowerCase() || '';
|
||||
if (arg && forbiddenElements.includes(nodeType)) {
|
||||
logWarning(`Plugin ${pluginId} tried to append ${nodeType}`, {
|
||||
pluginId,
|
||||
attrOrMethod: 'appendChild',
|
||||
param: nodeType,
|
||||
entity: 'HTMLElement',
|
||||
});
|
||||
|
||||
if (!monitorOnly) {
|
||||
return document.createDocumentFragment();
|
||||
}
|
||||
}
|
||||
if (isFunction(originalMethod)) {
|
||||
return originalMethod.call(this, arg);
|
||||
@ -284,6 +383,7 @@ function distortAppend(distortions: DistortionMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// this is not a distortion for security reasons but to make plugins using web workers work correctly.
|
||||
function distortWorkers(distortions: DistortionMap) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Worker.prototype, 'postMessage');
|
||||
function getPostMessageDistortion(originalMethod: unknown) {
|
||||
@ -306,6 +406,7 @@ function distortWorkers(distortions: DistortionMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// this is not a distortion for security reasons but to make plugins using document.defaultView work correctly.
|
||||
function distortDocument(distortions: DistortionMap) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'defaultView');
|
||||
if (descriptor?.get) {
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { isNearMembraneProxy, ProxyTarget } from '@locker/near-membrane-shared';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { forbiddenElements } from './constants';
|
||||
import { isReactClassComponent } from './utils';
|
||||
import { isReactClassComponent, logWarning } from './utils';
|
||||
|
||||
// IMPORTANT: NEVER export this symbol from a public (e.g `@grafana/*`) package
|
||||
const SANDBOX_LIVE_VALUE = Symbol.for('@@SANDBOX_LIVE_VALUE');
|
||||
const monitorOnly = Boolean(config.featureToggles.frontendSandboxMonitorOnly);
|
||||
|
||||
export function getSafeSandboxDomElement(element: Element, pluginId: string): Element {
|
||||
const nodeName = Reflect.get(element, 'nodeName');
|
||||
@ -26,7 +29,14 @@ export function getSafeSandboxDomElement(element: Element, pluginId: string): El
|
||||
}
|
||||
|
||||
if (forbiddenElements.includes(nodeName)) {
|
||||
throw new Error('<' + nodeName + '> is not allowed in sandboxed plugins');
|
||||
logWarning('<' + nodeName + '> is not allowed in sandboxed plugins', {
|
||||
pluginId,
|
||||
param: nodeName,
|
||||
});
|
||||
|
||||
if (!monitorOnly) {
|
||||
throw new Error('<' + nodeName + '> is not allowed in sandboxed plugins');
|
||||
}
|
||||
}
|
||||
|
||||
// allow elements inside the sandbox or the sandbox body
|
||||
@ -38,10 +48,15 @@ export function getSafeSandboxDomElement(element: Element, pluginId: string): El
|
||||
return element;
|
||||
}
|
||||
|
||||
// any other element gets a mock
|
||||
const mockElement = document.createElement(nodeName);
|
||||
mockElement.dataset.grafanaPluginSandboxElement = 'true';
|
||||
return mockElement;
|
||||
if (!monitorOnly) {
|
||||
// any other element gets a mock
|
||||
const mockElement = document.createElement(nodeName);
|
||||
mockElement.dataset.grafanaPluginSandboxElement = 'true';
|
||||
// we are not logging this because a high number of warnings can be generated
|
||||
return mockElement;
|
||||
} else {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
export function isDomElement(obj: unknown): obj is Element {
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
import { sandboxPluginDependencies } from './plugin_dependencies';
|
||||
import { sandboxPluginComponents } from './sandbox_components';
|
||||
import { CompartmentDependencyModule, PluginFactoryFunction } from './types';
|
||||
import { logError } from './utils';
|
||||
|
||||
// Loads near membrane custom formatter for near membrane proxy objects.
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
@ -34,7 +35,12 @@ export async function importPluginModuleInSandbox({ pluginId }: { pluginId: stri
|
||||
}
|
||||
return pluginImportCache.get(pluginId);
|
||||
} catch (e) {
|
||||
throw new Error(`Could not import plugin ${pluginId} inside sandbox: ` + e);
|
||||
const error = new Error(`Could not import plugin ${pluginId} inside sandbox: ` + e);
|
||||
logError(error, {
|
||||
pluginId,
|
||||
error: String(e),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +62,7 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise<unknown>
|
||||
}
|
||||
const distortion = generalDistortionMap.get(originalValue);
|
||||
if (distortion) {
|
||||
return distortion(originalValue) as ProxyTarget;
|
||||
return distortion(originalValue, meta.id) as ProxyTarget;
|
||||
}
|
||||
return originalValue;
|
||||
}
|
||||
@ -100,7 +106,12 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise<unknown>
|
||||
const pluginExports = await sandboxPluginComponents(pluginExportsRaw, meta);
|
||||
resolve(pluginExports);
|
||||
} catch (e) {
|
||||
reject(new Error(`Could not execute plugin ${meta.id}: ` + e));
|
||||
const error = new Error(`Could not execute plugin's define ${meta.id}: ` + e);
|
||||
logError(error, {
|
||||
pluginId: meta.id,
|
||||
error: String(e),
|
||||
});
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
@ -137,7 +148,12 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise<unknown>
|
||||
// of endowments.
|
||||
sandboxEnvironment.evaluate(pluginCode);
|
||||
} catch (e) {
|
||||
reject(new Error(`Could not execute plugin ${meta.id}: ` + e));
|
||||
const error = new Error(`Could not run plugin ${meta.id} inside sandbox: ` + e);
|
||||
logError(error, {
|
||||
pluginId: meta.id,
|
||||
error: String(e),
|
||||
});
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import { LogContext } from '@grafana/faro-web-sdk';
|
||||
import { logWarning as logWarningRuntime, logError as logErrorRuntime, config } from '@grafana/runtime';
|
||||
|
||||
import { SandboxedPluginObject } from './types';
|
||||
|
||||
const monitorOnly = Boolean(config.featureToggles.frontendSandboxMonitorOnly);
|
||||
|
||||
export function isSandboxedPluginObject(value: unknown): value is SandboxedPluginObject {
|
||||
return !!value && typeof value === 'object' && value?.hasOwnProperty('plugin');
|
||||
}
|
||||
@ -13,3 +18,21 @@ export function assertNever(x: never): never {
|
||||
export function isReactClassComponent(obj: unknown): obj is React.Component {
|
||||
return obj instanceof React.Component;
|
||||
}
|
||||
|
||||
export function logWarning(message: string, context?: LogContext) {
|
||||
context = {
|
||||
...context,
|
||||
source: 'sandbox',
|
||||
monitorOnly: String(monitorOnly),
|
||||
};
|
||||
logWarningRuntime(message, context);
|
||||
}
|
||||
|
||||
export function logError(error: Error, context?: LogContext) {
|
||||
context = {
|
||||
...context,
|
||||
source: 'sandbox',
|
||||
monitorOnly: String(monitorOnly),
|
||||
};
|
||||
logErrorRuntime(error, context);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user