mirror of
https://github.com/apache/superset.git
synced 2026-06-10 10:09:14 +00:00
Compare commits
2 Commits
url-param-
...
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 = {
|
||||
/** 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;
|
||||
/** The domain where Superset can be located, with protocol, such as: https://superset.example.com */
|
||||
supersetDomain: string;
|
||||
@@ -112,6 +114,48 @@ export type EmbeddedDashboard = {
|
||||
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.
|
||||
*/
|
||||
@@ -128,6 +172,8 @@ export async function embedDashboard({
|
||||
referrerPolicy,
|
||||
resolvePermalinkUrl,
|
||||
}: EmbedDashboardParams): Promise<EmbeddedDashboard> {
|
||||
validateEmbeddedDashboardId(id);
|
||||
|
||||
function log(...info: unknown[]) {
|
||||
if (debug) {
|
||||
console.debug(`[superset-embedded-sdk][dashboard ${id}]`, ...info);
|
||||
@@ -139,6 +185,7 @@ export async function embedDashboard({
|
||||
if (supersetDomain.endsWith('/')) {
|
||||
supersetDomain = supersetDomain.slice(0, -1);
|
||||
}
|
||||
validateSupersetDomain(supersetDomain);
|
||||
|
||||
function calculateConfig() {
|
||||
let configNumber = 0;
|
||||
@@ -195,6 +242,13 @@ export async function embedDashboard({
|
||||
iframe.sandbox.add('allow-forms'); // for forms to submit
|
||||
iframe.sandbox.add('allow-popups'); // for exporting charts as csv
|
||||
// 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) => {
|
||||
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.
|
||||
iframe.contentWindow!.postMessage(
|
||||
{ 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],
|
||||
);
|
||||
log('sent message channel to the iframe');
|
||||
|
||||
Reference in New Issue
Block a user