mirror of
https://github.com/apache/superset.git
synced 2026-06-11 18:49:15 +00:00
Compare commits
2 Commits
fix/chart-
...
fix/embedd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97b6465ece | ||
|
|
4f2fe42044 |
24
superset-embedded-sdk/jest.config.js
Normal file
24
superset-embedded-sdk/jest.config.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// @superset-ui/switchboard ships ES module syntax, so it must be
|
||||||
|
// transformed by babel rather than ignored as a node_modules dependency.
|
||||||
|
transformIgnorePatterns: ["/node_modules/(?!@superset-ui/switchboard)"],
|
||||||
|
};
|
||||||
91
superset-embedded-sdk/src/index.test.ts
Normal file
91
superset-embedded-sdk/src/index.test.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
validateEmbeddedDashboardId,
|
||||||
|
validateSupersetDomain,
|
||||||
|
findUnsafeSandboxExtras,
|
||||||
|
} from "./index";
|
||||||
|
|
||||||
|
describe("validateEmbeddedDashboardId", () => {
|
||||||
|
it("accepts a canonical uuid", () => {
|
||||||
|
expect(() =>
|
||||||
|
validateEmbeddedDashboardId("f4787a4f-2541-4f8a-9b5e-1e2d3c4b5a6f")
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a simple hexadecimal id", () => {
|
||||||
|
expect(() => validateEmbeddedDashboardId("abc123")).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["../../evil"],
|
||||||
|
["a/b"],
|
||||||
|
["x?foo=bar"],
|
||||||
|
["x#frag"],
|
||||||
|
["a@b.com"],
|
||||||
|
["foo bar"],
|
||||||
|
["http://evil.com"],
|
||||||
|
[""],
|
||||||
|
["%2e%2e"],
|
||||||
|
])("rejects an id with an unexpected format: %p", (id) => {
|
||||||
|
expect(() => validateEmbeddedDashboardId(id)).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateSupersetDomain", () => {
|
||||||
|
it.each([
|
||||||
|
["https://superset.example.com"],
|
||||||
|
["http://localhost:8088"],
|
||||||
|
// sub-path deployments are valid; the origin is what matters downstream
|
||||||
|
["https://example.com/superset"],
|
||||||
|
])("accepts a valid absolute URL: %p", (domain) => {
|
||||||
|
expect(() => validateSupersetDomain(domain)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["superset.example.com"], // missing protocol
|
||||||
|
["not a url"],
|
||||||
|
[""],
|
||||||
|
["/relative/path"],
|
||||||
|
])("rejects a malformed domain: %p", (domain) => {
|
||||||
|
expect(() => validateSupersetDomain(domain)).toThrow(
|
||||||
|
"Invalid supersetDomain format"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findUnsafeSandboxExtras", () => {
|
||||||
|
it("returns the tokens that relax iframe isolation", () => {
|
||||||
|
expect(
|
||||||
|
findUnsafeSandboxExtras([
|
||||||
|
"allow-forms",
|
||||||
|
"allow-top-navigation",
|
||||||
|
"allow-popups",
|
||||||
|
"allow-top-navigation-by-user-activation",
|
||||||
|
])
|
||||||
|
).toEqual(["allow-top-navigation", "allow-top-navigation-by-user-activation"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array when all tokens are safe", () => {
|
||||||
|
expect(
|
||||||
|
findUnsafeSandboxExtras(["allow-forms", "allow-popups", "allow-downloads"])
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -50,7 +50,9 @@ export type UiConfigType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type EmbedDashboardParams = {
|
export type EmbedDashboardParams = {
|
||||||
/** The id provided by the embed configuration UI in Superset */
|
/** The id provided by the embed configuration UI in Superset.
|
||||||
|
* This is the UUID generated by Superset's embed configuration and is
|
||||||
|
* expected to contain only hexadecimal characters and hyphens. */
|
||||||
id: string;
|
id: string;
|
||||||
/** The domain where Superset can be located, with protocol, such as: https://superset.example.com */
|
/** The domain where Superset can be located, with protocol, such as: https://superset.example.com */
|
||||||
supersetDomain: string;
|
supersetDomain: string;
|
||||||
@@ -112,6 +114,48 @@ export type EmbeddedDashboard = {
|
|||||||
setThemeMode: (mode: ThemeMode) => void;
|
setThemeMode: (mode: ThemeMode) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that an embedded dashboard id has the expected format
|
||||||
|
* (the UUID produced by Superset's embed configuration). Throws on
|
||||||
|
* anything containing characters that are not part of that format.
|
||||||
|
*/
|
||||||
|
export function validateEmbeddedDashboardId(id: string): void {
|
||||||
|
if (typeof id !== 'string' || !/^[a-f0-9-]+$/i.test(id)) {
|
||||||
|
throw new Error('Invalid dashboard id format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that supersetDomain is a parseable absolute URL (it must include
|
||||||
|
* a protocol, e.g. https://superset.example.com). Throws otherwise. The
|
||||||
|
* domain's origin is what gets used as the postMessage targetOrigin, so it
|
||||||
|
* has to resolve to a well-formed origin.
|
||||||
|
*/
|
||||||
|
export function validateSupersetDomain(supersetDomain: string): void {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new URL(supersetDomain);
|
||||||
|
} catch {
|
||||||
|
throw new Error('Invalid supersetDomain format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sandbox tokens that materially relax the iframe's isolation (for example,
|
||||||
|
// letting the embedded frame navigate the top-level page). They remain
|
||||||
|
// supported via iframeSandboxExtras for callers that genuinely need them.
|
||||||
|
const UNSAFE_SANDBOX_EXTRAS = [
|
||||||
|
'allow-top-navigation',
|
||||||
|
'allow-top-navigation-by-user-activation',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns any caller-provided sandbox tokens that relax the iframe's
|
||||||
|
* isolation, so they can be surfaced and not enabled unintentionally.
|
||||||
|
*/
|
||||||
|
export function findUnsafeSandboxExtras(extras: string[]): string[] {
|
||||||
|
return extras.filter(token => UNSAFE_SANDBOX_EXTRAS.includes(token));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Embeds a Superset dashboard into the page using an iframe.
|
* Embeds a Superset dashboard into the page using an iframe.
|
||||||
*/
|
*/
|
||||||
@@ -128,6 +172,8 @@ export async function embedDashboard({
|
|||||||
referrerPolicy,
|
referrerPolicy,
|
||||||
resolvePermalinkUrl,
|
resolvePermalinkUrl,
|
||||||
}: EmbedDashboardParams): Promise<EmbeddedDashboard> {
|
}: EmbedDashboardParams): Promise<EmbeddedDashboard> {
|
||||||
|
validateEmbeddedDashboardId(id);
|
||||||
|
|
||||||
function log(...info: unknown[]) {
|
function log(...info: unknown[]) {
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.debug(`[superset-embedded-sdk][dashboard ${id}]`, ...info);
|
console.debug(`[superset-embedded-sdk][dashboard ${id}]`, ...info);
|
||||||
@@ -139,6 +185,7 @@ export async function embedDashboard({
|
|||||||
if (supersetDomain.endsWith('/')) {
|
if (supersetDomain.endsWith('/')) {
|
||||||
supersetDomain = supersetDomain.slice(0, -1);
|
supersetDomain = supersetDomain.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
validateSupersetDomain(supersetDomain);
|
||||||
|
|
||||||
function calculateConfig() {
|
function calculateConfig() {
|
||||||
let configNumber = 0;
|
let configNumber = 0;
|
||||||
@@ -195,6 +242,13 @@ export async function embedDashboard({
|
|||||||
iframe.sandbox.add('allow-forms'); // for forms to submit
|
iframe.sandbox.add('allow-forms'); // for forms to submit
|
||||||
iframe.sandbox.add('allow-popups'); // for exporting charts as csv
|
iframe.sandbox.add('allow-popups'); // for exporting charts as csv
|
||||||
// additional sandbox props
|
// additional sandbox props
|
||||||
|
const unsafeSandboxExtras = findUnsafeSandboxExtras(iframeSandboxExtras);
|
||||||
|
if (unsafeSandboxExtras.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
'[superset-embedded-sdk] iframeSandboxExtras includes tokens that ' +
|
||||||
|
`relax the iframe's isolation: ${unsafeSandboxExtras.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
iframeSandboxExtras.forEach((key: string) => {
|
iframeSandboxExtras.forEach((key: string) => {
|
||||||
iframe.sandbox.add(key);
|
iframe.sandbox.add(key);
|
||||||
});
|
});
|
||||||
@@ -216,7 +270,9 @@ export async function embedDashboard({
|
|||||||
// we know the content window isn't null because we are in the load event handler.
|
// we know the content window isn't null because we are in the load event handler.
|
||||||
iframe.contentWindow!.postMessage(
|
iframe.contentWindow!.postMessage(
|
||||||
{ type: IFRAME_COMMS_MESSAGE_TYPE, handshake: 'port transfer' },
|
{ type: IFRAME_COMMS_MESSAGE_TYPE, handshake: 'port transfer' },
|
||||||
supersetDomain,
|
// Use the normalized origin (not the raw domain, which may carry a
|
||||||
|
// sub-path for sub-path deployments) as the postMessage targetOrigin.
|
||||||
|
new URL(supersetDomain).origin,
|
||||||
[theirPort],
|
[theirPort],
|
||||||
);
|
);
|
||||||
log('sent message channel to the iframe');
|
log('sent message channel to the iframe');
|
||||||
|
|||||||
Reference in New Issue
Block a user