mirror of
https://github.com/grafana/grafana.git
synced 2024-11-21 08:34:25 -06:00
Frontend Sandbox: Create a plugin sandbox enable registry. Use enable list instead of disable list (#94809)
* Use a enable configuration to enable frontend sandbox * Modify settings to load enableFrontendSandbox * Check for signature type * Update commment * Fix e2e tests for the frontend sandbox * Modify logic so a custom check function is used instead of a list of checks * Fixes flaky test * fix comment * Update comment * Empty commit * Empty commit
This commit is contained in:
parent
beac7de4df
commit
f248a55576
@ -403,8 +403,9 @@ angular_support_enabled = false
|
||||
# The CSRF check will be executed even if the request has no login cookie.
|
||||
csrf_always_check = false
|
||||
|
||||
# Comma-separated list of plugins ids that won't be loaded inside the frontend sandbox
|
||||
disable_frontend_sandbox_for_plugins = grafana-incident-app
|
||||
# Comma-separated list of plugins ids that will be loaded inside the frontend sandbox
|
||||
# Currently behind the feature flag pluginsFrontendSandbox
|
||||
enable_frontend_sandbox_for_plugins =
|
||||
|
||||
# Comma-separated list of paths for POST/PUT URL in actions. Empty will allow anything that is not on the same origin
|
||||
actions_allow_post_url =
|
||||
|
@ -408,8 +408,9 @@
|
||||
# The CSRF check will be executed even if the request has no login cookie.
|
||||
;csrf_always_check = false
|
||||
|
||||
# Comma-separated list of plugins ids that won't be loaded inside the frontend sandbox
|
||||
;disable_frontend_sandbox_for_plugins =
|
||||
# Comma-separated list of plugins ids that will be loaded inside the frontend sandbox
|
||||
# Currently behind the feature flag pluginsFrontendSandbox
|
||||
;enable_frontend_sandbox_for_plugins =
|
||||
|
||||
# Comma-separated list of paths for POST/PUT URL in actions. Empty will allow anything that is not on the same origin
|
||||
;actions_allow_post_url =
|
||||
|
@ -729,10 +729,9 @@ List of allowed headers to be set by the user. Suggested to use for if authentic
|
||||
|
||||
Set to `true` to execute the CSRF check even if the login cookie is not in a request (default `false`).
|
||||
|
||||
### disable_frontend_sandbox_for_plugins
|
||||
### enable_frontend_sandbox_for_plugins
|
||||
|
||||
Comma-separated list of plugins ids that won't be loaded inside the frontend sandbox. It is recommended to only use this
|
||||
option for plugins that are known to have problems running inside the frontend sandbox.
|
||||
Comma-separated list of plugins ids that will be loaded inside the frontend sandbox.
|
||||
|
||||
## [snapshots]
|
||||
|
||||
|
@ -1,128 +0,0 @@
|
||||
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(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
|
||||
return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true);
|
||||
});
|
||||
|
||||
describe('Sandbox disabled', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0');
|
||||
});
|
||||
cy.reload();
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('Reaches out of the panel editor', () => {
|
||||
e2e.flows.openDashboard({
|
||||
uid: DASHBOARD_ID,
|
||||
queryParams: {
|
||||
editPanel: 1,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').should('not.be.disabled');
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x', { force: true });
|
||||
cy.get('[data-sandbox-test="panel-editor"]').should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sandbox enabled', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
|
||||
});
|
||||
cy.reload();
|
||||
});
|
||||
|
||||
it('Does not add iframes to body', () => {
|
||||
// this button adds 3 iframes to the body
|
||||
cy.get('[data-testid="button-create-iframes"]').click();
|
||||
cy.wait(100); // small delay to prevent false positives from too fast tests
|
||||
|
||||
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.wait(100); // small delay to prevent false positives from too fast tests
|
||||
cy.get('[data-sandbox-test="true"]').should('not.exist');
|
||||
});
|
||||
|
||||
it('Does not Reaches out of the panel editor', () => {
|
||||
e2e.flows.openDashboard({
|
||||
uid: DASHBOARD_ID,
|
||||
queryParams: {
|
||||
editPanel: 1,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').should('not.be.disabled');
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x', { force: true });
|
||||
cy.wait(100); // small delay to prevent false positives from too fast tests
|
||||
cy.get('[data-sandbox-test="panel-editor"]').should('not.exist');
|
||||
});
|
||||
|
||||
it('Can access specific window global variables', () => {
|
||||
cy.get('[data-testid="button-test-globals"]').click();
|
||||
cy.get('[data-sandbox-global="Prism"]').should('be.visible');
|
||||
cy.get('[data-sandbox-global="jQuery"]').should('be.visible');
|
||||
cy.get('[data-sandbox-global="location"]').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
e2e.flows.revertAllChanges();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return cy.clearCookies();
|
||||
});
|
||||
});
|
@ -1,71 +0,0 @@
|
||||
import { e2e } from '../utils';
|
||||
|
||||
const APP_ID = 'sandbox-app-test';
|
||||
|
||||
describe('Datasource sandbox', () => {
|
||||
before(() => {
|
||||
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
|
||||
cy.request({
|
||||
url: `${Cypress.env('BASE_URL')}/api/plugins/${APP_ID}/settings`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
|
||||
});
|
||||
|
||||
describe('App Page', () => {
|
||||
describe('Sandbox disabled', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0');
|
||||
});
|
||||
});
|
||||
|
||||
it('Loads the app page without the sandbox div wrapper', () => {
|
||||
cy.visit(`/a/${APP_ID}`);
|
||||
cy.wait(200); // wait to prevent false positives because cypress checks too fast
|
||||
cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('not.exist');
|
||||
cy.get('div[data-testid="sandbox-app-test-page-one"]').should('exist');
|
||||
});
|
||||
|
||||
it('Loads the app configuration without the sandbox div wrapper', () => {
|
||||
cy.visit(`/plugins/${APP_ID}`);
|
||||
cy.wait(200); // wait to prevent false positives because cypress checks too fast
|
||||
cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('not.exist');
|
||||
cy.get('div[data-testid="sandbox-app-test-config-page"]').should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sandbox enabled', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
|
||||
});
|
||||
});
|
||||
|
||||
it('Loads the app page with the sandbox div wrapper', () => {
|
||||
cy.visit(`/a/${APP_ID}`);
|
||||
cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('exist');
|
||||
cy.get('div[data-testid="sandbox-app-test-page-one"]').should('exist');
|
||||
});
|
||||
|
||||
it('Loads the app configuration with the sandbox div wrapper', () => {
|
||||
cy.visit(`/plugins/${APP_ID}`);
|
||||
cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('exist');
|
||||
cy.get('div[data-testid="sandbox-app-test-config-page"]').should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
e2e.flows.revertAllChanges();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.clearCookies();
|
||||
});
|
||||
});
|
@ -1,155 +0,0 @@
|
||||
import { random } from 'lodash';
|
||||
|
||||
import { e2e } from '../utils';
|
||||
|
||||
const DATASOURCE_ID = 'sandbox-test-datasource';
|
||||
let DATASOURCE_CONNECTION_ID = '';
|
||||
const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance';
|
||||
|
||||
describe('Datasource sandbox', () => {
|
||||
before(() => {
|
||||
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
|
||||
|
||||
e2e.pages.AddDataSource.visit();
|
||||
e2e.pages.AddDataSource.dataSourcePluginsV2('Sandbox datasource test plugin')
|
||||
.scrollIntoView()
|
||||
.should('be.visible') // prevents flakiness
|
||||
.click();
|
||||
e2e.pages.DataSource.name().clear();
|
||||
e2e.pages.DataSource.name().type(DATASOURCE_TYPED_NAME);
|
||||
e2e.pages.DataSource.saveAndTest().click();
|
||||
cy.url().then((url) => {
|
||||
const split = url.split('/');
|
||||
DATASOURCE_CONNECTION_ID = split[split.length - 1];
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
|
||||
});
|
||||
|
||||
describe('Config Editor', () => {
|
||||
describe('Sandbox disabled', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0');
|
||||
});
|
||||
});
|
||||
it('Should not render a sandbox wrapper around the datasource config editor', () => {
|
||||
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
|
||||
cy.wait(300); // wait to prevent false positives because cypress checks too fast
|
||||
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sandbox enabled', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should render a sandbox wrapper around the datasource config editor', () => {
|
||||
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
|
||||
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('exist');
|
||||
});
|
||||
|
||||
it('Should store values in jsonData and secureJsonData correctly', () => {
|
||||
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
|
||||
|
||||
const valueToStore = 'test' + random(100);
|
||||
|
||||
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('not.be.disabled');
|
||||
cy.get('[data-testid="sandbox-config-editor-query-input"]').type(valueToStore);
|
||||
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('have.value', valueToStore);
|
||||
|
||||
e2e.pages.DataSource.saveAndTest().click();
|
||||
e2e.pages.DataSource.alert().should('exist').contains('Sandbox Success', {});
|
||||
|
||||
// validate the value was stored
|
||||
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
|
||||
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('not.be.disabled');
|
||||
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('have.value', valueToStore);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Explore Page', () => {
|
||||
describe('Sandbox disabled', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not wrap the query editor in a sandbox wrapper', () => {
|
||||
e2e.pages.Explore.visit();
|
||||
e2e.components.DataSourcePicker.container().should('be.visible').click();
|
||||
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
|
||||
// make sure the datasource was correctly selected and rendered
|
||||
e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible');
|
||||
|
||||
cy.wait(300); // wait to prevent false positives because cypress checks too fast
|
||||
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('not.exist');
|
||||
});
|
||||
|
||||
it('Should accept values when typed', () => {
|
||||
e2e.pages.Explore.visit();
|
||||
e2e.components.DataSourcePicker.container().should('be.visible').click();
|
||||
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
|
||||
|
||||
// make sure the datasource was correctly selected and rendered
|
||||
e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible');
|
||||
|
||||
const valueToType = 'test' + random(100);
|
||||
|
||||
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('not.be.disabled');
|
||||
cy.get('[data-testid="sandbox-query-editor-query-input"]').type(valueToType);
|
||||
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('have.value', valueToType);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sandbox enabled', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should wrap the query editor in a sandbox wrapper', () => {
|
||||
e2e.pages.Explore.visit();
|
||||
e2e.components.DataSourcePicker.container().should('be.visible').click();
|
||||
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
|
||||
// make sure the datasource was correctly selected and rendered
|
||||
e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible');
|
||||
|
||||
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('exist');
|
||||
});
|
||||
|
||||
it('Should accept values when typed', () => {
|
||||
e2e.pages.Explore.visit();
|
||||
e2e.components.DataSourcePicker.container().should('be.visible').click();
|
||||
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
|
||||
|
||||
// make sure the datasource was correctly selected and rendered
|
||||
e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible');
|
||||
|
||||
const valueToType = 'test' + random(100);
|
||||
|
||||
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('not.be.disabled');
|
||||
cy.get('[data-testid="sandbox-query-editor-query-input"]').type(valueToType);
|
||||
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('have.value', valueToType);
|
||||
|
||||
// typing the query editor should reflect in the url
|
||||
cy.url().should('include', valueToType);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
e2e.flows.revertAllChanges();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.clearCookies();
|
||||
});
|
||||
});
|
@ -3,7 +3,7 @@ import { e2e } from '../utils';
|
||||
|
||||
const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950';
|
||||
// Skipping due to race conditions with same old arch test e2e/panels-suite/frontend-sandbox-panel.spec.ts
|
||||
describe.skip('Panel sandbox', () => {
|
||||
describe('Panel sandbox', () => {
|
||||
beforeEach(() => {
|
||||
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
|
||||
return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true);
|
||||
@ -54,7 +54,12 @@ describe.skip('Panel sandbox', () => {
|
||||
});
|
||||
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').should('not.be.disabled');
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x', { force: true });
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').should('have.value', '');
|
||||
// wait because sometimes cypress is faster than react and the value doesn't change
|
||||
cy.wait(1000);
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x', { force: true, delay: 500 });
|
||||
cy.wait(100); // small delay to prevent false positives from too fast tests
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').should('have.value', 'x');
|
||||
cy.get('[data-sandbox-test="panel-editor"]').should('exist');
|
||||
});
|
||||
});
|
||||
@ -105,6 +110,8 @@ describe.skip('Panel sandbox', () => {
|
||||
});
|
||||
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').should('not.be.disabled');
|
||||
// wait because sometimes cypress is faster than react and the value doesn't change
|
||||
cy.wait(1000);
|
||||
cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x', { force: true });
|
||||
cy.wait(100); // small delay to prevent false positives from too fast tests
|
||||
cy.get('[data-sandbox-test="panel-editor"]').should('not.exist');
|
||||
|
@ -7,7 +7,7 @@ let DATASOURCE_CONNECTION_ID = '';
|
||||
const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance';
|
||||
|
||||
// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/frontend-sandbox-datasource.spec.ts
|
||||
describe.skip('Datasource sandbox', () => {
|
||||
describe('Datasource sandbox', () => {
|
||||
before(() => {
|
||||
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
|
||||
|
||||
|
@ -189,7 +189,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
};
|
||||
|
||||
tokenExpirationDayLimit: undefined;
|
||||
disableFrontendSandboxForPlugins: string[] = [];
|
||||
enableFrontendSandboxForPlugins: string[] = [];
|
||||
sharedWithMeFolderUID: string | undefined;
|
||||
rootFolderUID: string | undefined;
|
||||
localFileSystemAvailable: boolean | undefined;
|
||||
|
@ -203,7 +203,7 @@ type FrontendSettingsDTO struct {
|
||||
DisableSanitizeHtml bool `json:"disableSanitizeHtml"`
|
||||
TrustedTypesDefaultPolicyEnabled bool `json:"trustedTypesDefaultPolicyEnabled"`
|
||||
CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"`
|
||||
DisableFrontendSandboxForPlugins []string `json:"disableFrontendSandboxForPlugins"`
|
||||
EnableFrontendSandboxForPlugins []string `json:"enableFrontendSandboxForPlugins"`
|
||||
ExploreDefaultTimeOffset string `json:"exploreDefaultTimeOffset"`
|
||||
|
||||
Auth FrontendSettingsAuthDTO `json:"auth"`
|
||||
|
@ -231,7 +231,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
CSPReportOnlyEnabled: hs.Cfg.CSPReportOnlyEnabled,
|
||||
DateFormats: hs.Cfg.DateFormats,
|
||||
SecureSocksDSProxyEnabled: hs.Cfg.SecureSocksDSProxy.Enabled && hs.Cfg.SecureSocksDSProxy.ShowUI,
|
||||
DisableFrontendSandboxForPlugins: hs.Cfg.DisableFrontendSandboxForPlugins,
|
||||
EnableFrontendSandboxForPlugins: hs.Cfg.EnableFrontendSandboxForPlugins,
|
||||
PublicDashboardAccessToken: c.PublicDashboardAccessToken,
|
||||
PublicDashboardsEnabled: hs.Cfg.PublicDashboardsEnabled,
|
||||
CloudMigrationIsTarget: isCloudMigrationTarget,
|
||||
|
@ -175,12 +175,12 @@ type Cfg struct {
|
||||
// CSPReportEnabled toggles Content Security Policy Report Only support.
|
||||
CSPReportOnlyEnabled bool
|
||||
// CSPReportOnlyTemplate contains the Content Security Policy Report Only template.
|
||||
CSPReportOnlyTemplate string
|
||||
AngularSupportEnabled bool
|
||||
DisableFrontendSandboxForPlugins []string
|
||||
DisableGravatar bool
|
||||
DataProxyWhiteList map[string]bool
|
||||
ActionsAllowPostURL string
|
||||
CSPReportOnlyTemplate string
|
||||
AngularSupportEnabled bool
|
||||
EnableFrontendSandboxForPlugins []string
|
||||
DisableGravatar bool
|
||||
DataProxyWhiteList map[string]bool
|
||||
ActionsAllowPostURL string
|
||||
|
||||
TempDataLifetime time.Duration
|
||||
|
||||
@ -1555,10 +1555,10 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
|
||||
cfg.CSPReportOnlyEnabled = security.Key("content_security_policy_report_only").MustBool(false)
|
||||
cfg.CSPReportOnlyTemplate = security.Key("content_security_policy_report_only_template").MustString("")
|
||||
|
||||
disableFrontendSandboxForPlugins := security.Key("disable_frontend_sandbox_for_plugins").MustString("")
|
||||
for _, plug := range strings.Split(disableFrontendSandboxForPlugins, ",") {
|
||||
enableFrontendSandboxForPlugins := security.Key("enable_frontend_sandbox_for_plugins").MustString("")
|
||||
for _, plug := range strings.Split(enableFrontendSandboxForPlugins, ",") {
|
||||
plug = strings.TrimSpace(plug)
|
||||
cfg.DisableFrontendSandboxForPlugins = append(cfg.DisableFrontendSandboxForPlugins, plug)
|
||||
cfg.EnableFrontendSandboxForPlugins = append(cfg.EnableFrontendSandboxForPlugins, plug)
|
||||
}
|
||||
|
||||
if cfg.CSPEnabled && cfg.CSPTemplate == "" {
|
||||
|
@ -22,7 +22,7 @@ import { decorateSystemJSFetch, decorateSystemJSResolve, decorateSystemJsOnload
|
||||
import { SystemJSWithLoaderHooks } from './loader/types';
|
||||
import { buildImportMap, resolveModulePath } from './loader/utils';
|
||||
import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader';
|
||||
import { isFrontendSandboxSupported } from './sandbox/utils';
|
||||
import { shouldLoadPluginInFrontendSandbox } from './sandbox/sandbox_plugin_loader_registry';
|
||||
import { pluginsLogger } from './utils';
|
||||
|
||||
const imports = buildImportMap(sharedDependenciesMap);
|
||||
@ -115,7 +115,7 @@ export async function importPluginModule({
|
||||
}
|
||||
|
||||
// the sandboxing environment code cannot work in nodejs and requires a real browser
|
||||
if (await isFrontendSandboxSupported({ isAngular, pluginId })) {
|
||||
if (await shouldLoadPluginInFrontendSandbox({ isAngular, pluginId })) {
|
||||
return importPluginModuleInSandbox({ pluginId });
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,92 @@
|
||||
import { PluginMeta, PluginSignatureType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { getPluginSettings } from '../pluginSettings';
|
||||
|
||||
import {
|
||||
shouldLoadPluginInFrontendSandbox,
|
||||
setSandboxEnabledCheck,
|
||||
isPluginFrontendSandboxEnabled,
|
||||
} from './sandbox_plugin_loader_registry';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
config: {
|
||||
featureToggles: { pluginsFrontendSandbox: true },
|
||||
buildInfo: { env: 'production' },
|
||||
enableFrontendSandboxForPlugins: [],
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../pluginSettings', () => ({
|
||||
getPluginSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
const getPluginSettingsMock = getPluginSettings as jest.MockedFunction<typeof getPluginSettings>;
|
||||
|
||||
const fakePlugin: PluginMeta = {
|
||||
id: 'test-plugin',
|
||||
name: 'Test Plugin',
|
||||
} as PluginMeta;
|
||||
|
||||
describe('Sandbox eligibility checks', () => {
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
config.enableFrontendSandboxForPlugins = [];
|
||||
config.featureToggles.pluginsFrontendSandbox = true;
|
||||
process.env.NODE_ENV = 'development';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalNodeEnv;
|
||||
});
|
||||
|
||||
test('shouldLoadPluginInFrontendSandbox returns false for Angular plugins', async () => {
|
||||
const result = await shouldLoadPluginInFrontendSandbox({ isAngular: true, pluginId: 'test-plugin' });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('shouldLoadPluginInFrontendSandbox returns false when feature toggle is off', async () => {
|
||||
config.featureToggles.pluginsFrontendSandbox = false;
|
||||
const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('shouldLoadPluginInFrontendSandbox returns false for Grafana-signed plugins', async () => {
|
||||
getPluginSettingsMock.mockResolvedValue({ ...fakePlugin, signatureType: PluginSignatureType.grafana });
|
||||
const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('shouldLoadPluginInFrontendSandbox returns true for eligible plugins in the list', async () => {
|
||||
getPluginSettingsMock.mockResolvedValue({ ...fakePlugin, signatureType: PluginSignatureType.community });
|
||||
config.enableFrontendSandboxForPlugins = ['test-plugin'];
|
||||
const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('isPluginFrontendSandboxEnabled returns false when plugin is not in the enabled list', async () => {
|
||||
config.enableFrontendSandboxForPlugins = ['other-plugin'];
|
||||
const result = await isPluginFrontendSandboxEnabled({ pluginId: 'test-plugin' });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('setSandboxEnabledCheck sets custom check function', async () => {
|
||||
const customCheck = jest.fn().mockResolvedValue(true);
|
||||
setSandboxEnabledCheck(customCheck);
|
||||
const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' });
|
||||
expect(customCheck).toHaveBeenCalledWith({ pluginId: 'test-plugin' });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('setSandboxEnabledCheck has precedence over default', async () => {
|
||||
const customCheck = jest.fn().mockResolvedValue(false);
|
||||
setSandboxEnabledCheck(customCheck);
|
||||
// this should be ignored by the custom check
|
||||
config.enableFrontendSandboxForPlugins = ['test-plugin'];
|
||||
const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' });
|
||||
expect(customCheck).toHaveBeenCalledWith({ pluginId: 'test-plugin' });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
@ -0,0 +1,78 @@
|
||||
import { PluginSignatureType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { getPluginSettings } from '../pluginSettings';
|
||||
|
||||
type SandboxEligibilityCheckParams = {
|
||||
isAngular?: boolean;
|
||||
pluginId: string;
|
||||
};
|
||||
|
||||
type SandboxEnabledCheck = (params: SandboxEligibilityCheckParams) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* We allow core extensions to register their own
|
||||
* sandbox enabled checks.
|
||||
*/
|
||||
let sandboxEnabledCheck: SandboxEnabledCheck = isPluginFrontendSandboxEnabled;
|
||||
|
||||
export function setSandboxEnabledCheck(checker: SandboxEnabledCheck) {
|
||||
sandboxEnabledCheck = checker;
|
||||
}
|
||||
|
||||
export async function shouldLoadPluginInFrontendSandbox({
|
||||
isAngular,
|
||||
pluginId,
|
||||
}: SandboxEligibilityCheckParams): Promise<boolean> {
|
||||
// basic check if the plugin is eligible for the sandbox
|
||||
if (!(await isPluginFrontendSandboxElegible({ isAngular, pluginId }))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return sandboxEnabledCheck({ isAngular, pluginId });
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a basic check that checks if the plugin is eligible to run in the sandbox.
|
||||
* It does not check if the plugin is actually enabled for the sandbox.
|
||||
*/
|
||||
async function isPluginFrontendSandboxElegible({
|
||||
isAngular,
|
||||
pluginId,
|
||||
}: SandboxEligibilityCheckParams): Promise<boolean> {
|
||||
// Only if the feature is not enabled no support for sandbox
|
||||
if (!Boolean(config.featureToggles.pluginsFrontendSandbox)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// no support for angular plugins
|
||||
if (isAngular) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// To fast-test and debug the sandbox in the browser (dev mode only).
|
||||
const sandboxDisableQueryParam = location.search.includes('nosandbox') && config.buildInfo.env === 'development';
|
||||
if (sandboxDisableQueryParam) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// no sandbox in test mode. it often breaks e2e tests
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// don't run grafana-signed plugins in sandbox
|
||||
const pluginMeta = await getPluginSettings(pluginId);
|
||||
if (pluginMeta.signatureType === PluginSignatureType.grafana) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the plugin is enabled for the sandbox via configuration.
|
||||
*/
|
||||
export async function isPluginFrontendSandboxEnabled({ pluginId }: SandboxEligibilityCheckParams): Promise<boolean> {
|
||||
return Boolean(config.enableFrontendSandboxForPlugins?.includes(pluginId));
|
||||
}
|
@ -2,12 +2,9 @@ import { isNearMembraneProxy } from '@locker/near-membrane-shared';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import * as React from 'react';
|
||||
|
||||
import { PluginSignatureType, PluginType } from '@grafana/data';
|
||||
import { LogContext } from '@grafana/faro-web-sdk';
|
||||
import { config, createMonitoringLogger } from '@grafana/runtime';
|
||||
|
||||
import { getPluginSettings } from '../pluginSettings';
|
||||
|
||||
import { SandboxedPluginObject } from './types';
|
||||
|
||||
const monitorOnly = Boolean(config.featureToggles.frontendSandboxMonitorOnly);
|
||||
@ -38,49 +35,6 @@ export function logInfo(message: string, context?: LogContext) {
|
||||
sandboxLogger.logInfo(message, context);
|
||||
}
|
||||
|
||||
export async function isFrontendSandboxSupported({
|
||||
isAngular,
|
||||
pluginId,
|
||||
}: {
|
||||
isAngular?: boolean;
|
||||
pluginId: string;
|
||||
}): Promise<boolean> {
|
||||
// Only if the feature is not enabled no support for sandbox
|
||||
if (!Boolean(config.featureToggles.pluginsFrontendSandbox)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// no support for angular plugins
|
||||
if (isAngular) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// To fast test and debug the sandbox in the browser.
|
||||
const sandboxDisableQueryParam = location.search.includes('nosandbox') && config.buildInfo.env === 'development';
|
||||
if (sandboxDisableQueryParam) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if disabled by configuration
|
||||
const isPluginExcepted = config.disableFrontendSandboxForPlugins.includes(pluginId);
|
||||
if (isPluginExcepted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// no sandbox in test mode. it often breaks e2e tests
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we don't run grafana-own apps in the sandbox
|
||||
const pluginMeta = await getPluginSettings(pluginId);
|
||||
if (pluginMeta.type === PluginType.app && pluginMeta.signatureType === PluginSignatureType.grafana) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isRegex(value: unknown): value is RegExp {
|
||||
return value?.constructor?.name === 'RegExp';
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
[security]
|
||||
content_security_policy = true
|
||||
content_security_policy_template = """require-trusted-types-for 'script'; script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
|
||||
enable_frontend_sandbox_for_plugins = sandbox-app-test,sandbox-test-datasource,sandbox-test-panel
|
||||
|
||||
[feature_toggles]
|
||||
enable = publicDashboards
|
||||
|
Loading…
Reference in New Issue
Block a user