From 95889b2e25ee45dd879337e791f97f833e2777ea Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 26 Sep 2023 10:58:47 +0200 Subject: [PATCH] Tests: Add basic e2e tests for frontend plugin sandbox (#70759) * Add initial e2e tests for sandboxing * Add tests for sandbox on * Add additional sandbox tests * Move sandbox into various suite * Test drone setup * Move variable * Update drone * Update plugins path for e2e * Revert drone changes * use drone from main * Use lib.star from main * Move sandbox test to its own suite * Expand methods to inject iframes * Restore e2e script * Add back change to script * Update tests for trusted types * Integrate custom plugins into grafana-server * Echo for deubging * add debugging message * Expand message * Add extra for ci * fix path * Improve start-server logic * Remove duplicated logic * Restore file deleted by mistake * Restore file to main p * restore file * Restore start script * Update e2e/panels-suite/frontend-sandbox-panel.spec.ts Co-authored-by: Levente Balogh --------- Co-authored-by: Levente Balogh --- .../frontend-sandbox-panel-test/module.js | 125 ++++++++++++++++++ .../frontend-sandbox-panel-test/plugin.json | 25 ++++ e2e/dashboards/PanelSandboxDashboard.json | 58 ++++++++ .../frontend-sandbox-panel.spec.ts | 100 ++++++++++++++ .../plugins/sandbox/sandbox_plugin_loader.ts | 5 + scripts/grafana-server/start-server | 12 +- 6 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 e2e/custom-plugins/frontend-sandbox-panel-test/module.js create mode 100644 e2e/custom-plugins/frontend-sandbox-panel-test/plugin.json create mode 100644 e2e/dashboards/PanelSandboxDashboard.json create mode 100644 e2e/panels-suite/frontend-sandbox-panel.spec.ts diff --git a/e2e/custom-plugins/frontend-sandbox-panel-test/module.js b/e2e/custom-plugins/frontend-sandbox-panel-test/module.js new file mode 100644 index 00000000000..40f14ab14c7 --- /dev/null +++ b/e2e/custom-plugins/frontend-sandbox-panel-test/module.js @@ -0,0 +1,125 @@ +/* + * This is a dummy plugin to test the frontend sandbox + * It is not meant to be used in any other way + * This file doesn't require any compilation + */ +define(['react', '@grafana/data'], function (React, grafanaData) { + const HelloWorld = () => { + const createIframe = () => { + // direct iframe creation + const iframe = document.createElement('iframe'); + iframe.src = 'about:blank'; + iframe.id = 'createElementIframe'; + iframe.style.width = '10%'; + iframe.style.height = '10%'; + iframe.style.border = 'none'; + document.body.appendChild(iframe); + + // via innerHTML + const div = document.createElement('div'); + document.body.appendChild(div); + div.innerHTML = + ''; + + // via append + const appendIframe = document.createElement('iframe'); + appendIframe.src = 'about:blank'; + appendIframe.id = 'appendIframe'; + appendIframe.style.width = '10%'; + appendIframe.style.height = '10%'; + appendIframe.style.border = 'none'; + document.body.append(appendIframe); + + // via prepend + const prependIframe = document.createElement('iframe'); + prependIframe.src = 'about:blank'; + prependIframe.id = 'prependIframe'; + prependIframe.style.width = '10%'; + prependIframe.style.height = '10%'; + prependIframe.style.border = 'none'; + document.body.prepend(prependIframe); + + // via after + const referenceElementAfter = document.createElement('div'); + document.body.appendChild(referenceElementAfter); + const afterIframe = document.createElement('iframe'); + afterIframe.src = 'about:blank'; + afterIframe.id = 'afterIframe'; + afterIframe.style.width = '10%'; + afterIframe.style.height = '10%'; + afterIframe.style.border = 'none'; + referenceElementAfter.after(afterIframe); + + // via before + const referenceElementBefore = document.createElement('div'); + document.body.appendChild(referenceElementBefore); + const beforeIframe = document.createElement('iframe'); + beforeIframe.src = 'about:blank'; + beforeIframe.id = 'beforeIframe'; + beforeIframe.style.width = '10%'; + beforeIframe.style.height = '10%'; + beforeIframe.style.border = 'none'; + referenceElementBefore.before(beforeIframe); + + // via outerHTML + const outerHTMLIframe = document.createElement('iframe'); + outerHTMLIframe.src = 'about:blank'; + outerHTMLIframe.id = 'outerHTMLIframeTemp'; + outerHTMLIframe.style.width = '10%'; + outerHTMLIframe.style.height = '10%'; + outerHTMLIframe.style.border = 'none'; + document.body.appendChild(outerHTMLIframe); + outerHTMLIframe.outerHTML = + ''; + + // via parseFromString + const iframeString = + ''; + const parser = new DOMParser(); + const parsedDoc = parser.parseFromString(iframeString, 'text/html'); + document.body.appendChild(parsedDoc.body.firstChild); + + // via insertBefore + const referenceForInsertBefore = document.createElement('div'); + document.body.appendChild(referenceForInsertBefore); + const insertBeforeIframe = document.createElement('iframe'); + insertBeforeIframe.src = 'about:blank'; + insertBeforeIframe.id = 'insertBeforeIframe'; + insertBeforeIframe.style.width = '10%'; + insertBeforeIframe.style.height = '10%'; + insertBeforeIframe.style.border = 'none'; + document.body.insertBefore(insertBeforeIframe, referenceForInsertBefore); + + // via replaceChild + const replaceChildDiv = document.createElement('div'); + document.body.appendChild(replaceChildDiv); + const replaceChildIframe = document.createElement('iframe'); + replaceChildIframe.src = 'about:blank'; + replaceChildIframe.id = 'replaceChildIframe'; + replaceChildIframe.style.width = '10%'; + replaceChildIframe.style.height = '10%'; + replaceChildIframe.style.border = 'none'; + document.body.replaceChild(replaceChildIframe, replaceChildDiv); + }; + + const reachOut = (e) => { + const outsideEl = e.target.parentElement.parentElement.parentElement.parentElement.parentElement; + outsideEl.dataset.sandboxTest = 'true'; + }; + + return React.createElement( + 'div', + { className: 'frontend-sandbox-test' }, + React.createElement( + 'button', + { onClick: createIframe, 'data-testid': 'button-create-iframes' }, + 'Create iframes' + ), + React.createElement('button', { onClick: reachOut, 'data-testid': 'button-reach-out' }, 'Reach out') + ); + }; + + const plugin = new grafanaData.PanelPlugin(HelloWorld); + + return { plugin }; +}); diff --git a/e2e/custom-plugins/frontend-sandbox-panel-test/plugin.json b/e2e/custom-plugins/frontend-sandbox-panel-test/plugin.json new file mode 100644 index 00000000000..a77283a0a6b --- /dev/null +++ b/e2e/custom-plugins/frontend-sandbox-panel-test/plugin.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json", + "type": "panel", + "name": "Sandbox test plugin", + "id": "sandbox-test-panel", + "info": { + "keywords": ["panel"], + "description": "", + "author": { + "name": "Grafana" + }, + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "links": [], + "screenshots": [], + "version": "1.0.0", + "updated": "2023-06-27" + }, + "dependencies": { + "grafanaDependency": ">=10.0", + "plugins": [] + } +} diff --git a/e2e/dashboards/PanelSandboxDashboard.json b/e2e/dashboards/PanelSandboxDashboard.json new file mode 100644 index 00000000000..0ea1ca81d24 --- /dev/null +++ b/e2e/dashboards/PanelSandboxDashboard.json @@ -0,0 +1,58 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 118, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "title": "Sandbox Panel test", + "type": "sandbox-test-panel" + } + ], + "refresh": "", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Sandbox Panel Test", + "uid": "c46b2460-16b7-42a5-82d1-b07fbf431950", + "version": 1, + "weekStart": "" +} diff --git a/e2e/panels-suite/frontend-sandbox-panel.spec.ts b/e2e/panels-suite/frontend-sandbox-panel.spec.ts new file mode 100644 index 00000000000..ccb037087e9 --- /dev/null +++ b/e2e/panels-suite/frontend-sandbox-panel.spec.ts @@ -0,0 +1,100 @@ +import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json'; +import { e2e } from '../utils'; + +const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950'; + +describe('Panel sandbox', () => { + beforeEach(() => { + e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD'), true); + return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true); + }); + + describe('Sandbox disabled', () => { + beforeEach(() => { + e2e.flows.openDashboard({ + uid: DASHBOARD_ID, + queryParams: { + '__feature.pluginsFrontendSandbox': false, + }, + }); + }); + + it('Add iframes to body', () => { + // this button adds iframes to the body + cy.get('[data-testid="button-create-iframes"]').click(); + + const iframeIds = [ + 'createElementIframe', + 'innerHTMLIframe', + 'appendIframe', + 'prependIframe', + 'afterIframe', + 'beforeIframe', + 'outerHTMLIframe', + 'parseFromStringIframe', + 'insertBeforeIframe', + 'replaceChildIframe', + ]; + iframeIds.forEach((id) => { + cy.get(`#${id}`).should('exist'); + }); + }); + + it('Reaches out of panel div', () => { + // this button reaches out of the panel div and modifies the element dataset + cy.get('[data-testid="button-reach-out"]').click(); + + cy.get('[data-sandbox-test="true"]').should('exist'); + }); + }); + + describe('Sandbox enabled', () => { + beforeEach(() => { + e2e.flows.openDashboard({ + uid: DASHBOARD_ID, + queryParams: { + '__feature.pluginsFrontendSandbox': true, + }, + }); + cy.window().then((win) => { + win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + }); + + it('Does not add iframes to body', () => { + // this button adds 3 iframes to the body + cy.get('[data-testid="button-create-iframes"]').click(); + + const iframeIds = [ + 'createElementIframe', + 'innerHTMLIframe', + 'appendIframe', + 'prependIframe', + 'afterIframe', + 'beforeIframe', + 'outerHTMLIframe', + 'parseFromStringIframe', + 'insertBeforeIframe', + 'replaceChildIframe', + ]; + iframeIds.forEach((id) => { + cy.get(`#${id}`).should('not.exist'); + }); + }); + + it('Does not reaches out of panel div', () => { + // this button reaches out of the panel div and modifies the element dataset + cy.get('[data-testid="button-reach-out"]').click(); + + cy.get('[data-sandbox-test="true"]').should('not.exist'); + }); + }); + + afterEach(() => { + e2e.flows.revertAllChanges(); + }); + + after(() => { + return cy.clearCookies(); + }); +}); diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts index 6b34a48c2c5..b9ea8fc584e 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts @@ -72,6 +72,11 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise string, + createScript: (string: string) => string, + createScriptURL: (string: string) => string, + }, liveTargetCallback: isLiveTarget, // endowments are custom variables we make available to plugins in their window object endowments: Object.getOwnPropertyDescriptors({ diff --git a/scripts/grafana-server/start-server b/scripts/grafana-server/start-server index 577210fa03d..2c804314dad 100755 --- a/scripts/grafana-server/start-server +++ b/scripts/grafana-server/start-server @@ -34,6 +34,17 @@ mkdir $PROV_DIR/dashboards cp ./scripts/grafana-server/custom.ini $RUNDIR/conf/custom.ini cp ./conf/defaults.ini $RUNDIR/conf/defaults.ini +echo -e "Copying custom plugins from e2e tests" + +mkdir -p "$RUNDIR/data/plugins" +# when running in a local computer +if [ -d "./e2e/custom-plugins" ]; then + cp -r "./e2e/custom-plugins" "$RUNDIR/data/plugins" +# when running in CI +elif [ -d "../e2e/custom-plugins" ]; then + cp -r "../e2e/custom-plugins" "$RUNDIR/data/plugins" +fi + echo -e "Copy provisioning setup from devenv" cp devenv/datasources.yaml $PROV_DIR/datasources @@ -53,4 +64,3 @@ $RUNDIR/bin/"$ARCH"grafana-server \ # 2>&1 > $RUNDIR/output.log & # cfg:log.level=debug \ -