Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand Down
4 changes: 4 additions & 0 deletions UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ assists people when migrating to a new version.
- `SAMPLES_ROW_LIMIT` is now the default for `/datasource/samples` requests without a valid explicit `per_page`, rather than a hard per-request ceiling; explicit limits are honored up to the existing global row-limit ceiling, matching `/chart/data` SAMPLES requests.
- The `cockroachdb` extra (`pip install apache-superset[cockroachdb]`) now installs `sqlalchemy-cockroachdb` instead of the abandoned `cockroachdb` package, whose SQLAlchemy dialect could not be imported under SQLAlchemy 2.0. Existing environments with the old package installed should `pip uninstall cockroachdb && pip install sqlalchemy-cockroachdb` (or simply reinstall the extra) to restore CockroachDB connectivity.

### Native Value filter "Select all" always targets the whole column

The native "Value" filter's bulk "Select all" / "Clear" controls now operate on the entire loaded set of column values regardless of any text typed into the filter's search box. Previously the "Select all (N)" count briefly flickered to the search-scoped count before settling on the full-column count, and clicking "Select all" while searching could select only the currently matching subset. Search-scoped bulk selection was never a supported feature; the count is now stable and always matches what "Select all" selects (the full column). No configuration change is required.

### MCP tool results preserve stored string values

Structured MCP tool results no longer add `<UNTRUSTED-CONTENT>` wrappers or
Expand Down
49 changes: 49 additions & 0 deletions docs/admin_docs/configuration/configuring-superset.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,55 @@ def FLASK_APP_MUTATOR(app: Flask) -> None:
app.before_request_funcs.setdefault(None, []).append(make_session_permanent)
```

## Carrying extra data through chart and dashboard exports

Deployments often attach their own metadata to charts and dashboards — an owning
team, a catalogue entry, a cost centre — and need it to survive an export/import
round trip between environments. `EXTRA_ASSET_EXPORT_FIELDS` and
`EXTRA_ASSET_IMPORT_HANDLER` let you do that without forking the export commands.

The export hook receives the model and the asset type (`"chart"` or `"dashboard"`)
and returns a mapping, which is serialised under the `extra` key of the asset's
YAML. The import hook receives the model, the asset type and that same mapping,
once the asset exists and has an id:

```python
# superset_config.py
def _export_fields(model, asset_type):
return {"owning_team": lookup_team(model)}


def _import_handler(model, asset_type, extra):
if team := extra.get("owning_team"):
assign_team(model, team)


EXTRA_ASSET_EXPORT_FIELDS = _export_fields
EXTRA_ASSET_IMPORT_HANDLER = _import_handler
```

The exported YAML then carries:

```yaml
slice_name: Revenue by region
...
extra:
owning_team: analytics-platform
```

A few things worth knowing:

- **Both hooks are optional and default to `None`.** With neither configured,
exported files are byte-for-byte what they were before, and imports behave
identically.
- **Everything lives under the single `extra` key.** The import schemas reject
unknown top-level fields, so namespacing under `extra` keeps that strictness
while leaving you free to change the shape of your own payload later.
- **An export hook returning `None` or an empty mapping writes nothing**, so
assets without your metadata do not gain an empty `extra` block.
- **The import handler runs after the asset is created or updated**, which means
you can rely on `model.id`. Raising from it will fail the import.

## Customizing the landing page (index view)

The page served at `/` is rendered by an index view. By default Superset registers
Expand Down
54 changes: 54 additions & 0 deletions docs/admin_docs/configuration/mcp-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,58 @@ def my_custom_auth_factory(app):
MCP_AUTH_FACTORY = my_custom_auth_factory
```

### Embedded Guest Authentication

Superset's [embedded dashboards](/user-docs/using-superset/embedding) feature mints short-lived **guest tokens** for anonymous/embedded viewers. The MCP server can accept these same guest tokens, so an embedded guest (e.g. an in-app chatbot next to an embedded dashboard) can call MCP tools scoped to the dashboards/resources named in its token.

This is opt-in and reuses the existing core guest-token configuration -- there is no MCP-specific guest secret or audience.

```python
# superset_config.py
FEATURE_FLAGS = {"EMBEDDED_SUPERSET": True} # required -- guest tokens only exist when this is on
MCP_EMBEDDED_GUEST_AUTH_ENABLED = True # opt-in for the MCP transport (default False)
```

Present the guest token the same way as any other bearer token:

```bash
curl -X POST http://localhost:5008/mcp \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_GUEST_TOKEN' \
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
```

**How it works**

- A dedicated guest-token verifier validates the token against the same `GUEST_TOKEN_JWT_SECRET` / `GUEST_TOKEN_JWT_ALGO` / `GUEST_TOKEN_JWT_AUDIENCE` config used by embedded dashboards, replays the embedded structural checks, and enforces revocation (global version bumps and per-dashboard `guest_token_revoked_before` cutoffs). It runs *before* the JWT verifier described above, since guest tokens are signed with a different key/algorithm and would otherwise be rejected at the transport.
- A verified guest resolves to a Superset guest user as the highest-priority identity, so it's never downgraded to API-key / `MCP_DEV_USERNAME` / dev-mode resolution. Data access is scoped by the same checks (dataset allowlist, dashboard access, row-level security) that apply to embedded dashboard views.
- Guests are restricted to a default-deny allow-list, `MCP_GUEST_ALLOWED_TOOLS`, regardless of `MCP_RBAC_ENABLED`. Sensitive enumeration tools like `find_users` and `get_instance_info` are denied simply by being absent from the default list.
- Setting `MCP_AUTH_FACTORY` bypasses this whole path: a configured factory is tried first, and the default factory that wires up the guest-token verifier is never reached. If you rely on a custom auth factory (e.g. your own OIDC provider) alongside guest auth, that factory must verify guest tokens itself -- otherwise they're rejected regardless of `MCP_EMBEDDED_GUEST_AUTH_ENABLED`.

```python
# superset_config.py
MCP_GUEST_ALLOWED_TOOLS = {
"get_dashboard_info",
"get_dashboard_layout",
"list_dashboards",
"list_charts",
"get_chart_info",
"get_chart_data",
"get_chart_preview",
} # default
```

**Deployment requirements**

- The MCP server and the service that mints guest tokens (the Superset web app) must share `GUEST_TOKEN_JWT_SECRET` and `GUEST_TOKEN_JWT_AUDIENCE`. Set `GUEST_TOKEN_JWT_AUDIENCE` explicitly -- if it's unset, audience validation falls back to the URL host, which can differ between the two services and cause every guest token to fail validation.
- The `GUEST_ROLE_NAME` role (default `Public`) must exist -- a guest token is rejected if it does not.
- Don't set `MCP_DEV_USERNAME` on a deployment that also serves embedded guests.
- Restart the MCP process after toggling `EMBEDDED_SUPERSET` or `MCP_EMBEDDED_GUEST_AUTH_ENABLED` -- guest auth is wired up once at startup.

:::warning
`GUEST_TOKEN_JWT_SECRET` guards both the web embedding and MCP guest-auth surfaces. With `MCP_EMBEDDED_GUEST_AUTH_ENABLED` on, leaving it at its insecure default isn't just a forgery risk -- the MCP server refuses to start (`MCPAuthConfigError`) until you set a real secret shared with the guest-token minting service.
:::

---

## Connecting AI Clients
Expand Down Expand Up @@ -523,6 +575,8 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
| `MCP_JWT_DEBUG_ERRORS` | `False` | Log detailed JWT errors server-side (never exposed in HTTP responses per RFC 6750) |
| `MCP_AUTH_FACTORY` | `None` | Custom auth provider factory `(flask_app) -> auth_provider`. Takes precedence over built-in JWT |
| `MCP_USER_RESOLVER` | `None` | Custom function `(app, access_token) -> username` to extract a Superset username from a validated JWT token. When `None`, the default resolver checks `preferred_username`, `username`, `email`, and `sub` claims in that order. |
| `MCP_EMBEDDED_GUEST_AUTH_ENABLED` | `False` | Accept embedded [guest tokens](#embedded-guest-authentication) as Bearer auth. Also requires the `EMBEDDED_SUPERSET` feature flag. |
| `MCP_GUEST_ALLOWED_TOOLS` | see [default list](#embedded-guest-authentication) | The only tool names callable by embedded guests (default-deny), regardless of `MCP_RBAC_ENABLED`. |

### Response Size Guard

Expand Down
33 changes: 33 additions & 0 deletions docs/admin_docs/configuration/theming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,39 @@ Font URLs are validated against a configurable allowlist. By default, fonts from

This feature works with the stock Docker image - no custom build required!

## Results Grid Configuration Overrides

Superset exposes a handful of opt-in tokens that customize the appearance of
the results grid in SQL Lab. These tokens have no effect unless explicitly
set, since the results grid otherwise falls back to its built-in defaults.

```python
THEME_DEFAULT = {
"token": {
"colorPrimary": "#2893B3",
# ... other Ant Design tokens

# Results grid overrides
"resultsGridRowHeight": 32,
"resultsGridHeaderFontSize": 13,
"resultsGridHeaderFontWeight": 600,
"resultsGridBorderRadius": 4,
"resultsGridNoStriping": True,
}
}
```

| Token | Type | Description |
| --- | --- | --- |
| `resultsGridRowHeight` | `number` | Row and header height, in pixels. |
| `resultsGridHeaderFontSize` | `number` | Header cell font size, in pixels. |
| `resultsGridHeaderFontWeight` | `number` | Header cell font weight. |
| `resultsGridBorderRadius` | `number` | Border radius applied to the grid and its wrapper, in pixels. |
| `resultsGridNoStriping` | `boolean` | When `true`, disables alternating row background striping. |

These tokens can also be set through the theme CRUD interface's JSON editor,
alongside any other Superset-specific tokens.

## ECharts Configuration Overrides

:::note
Expand Down
6 changes: 3 additions & 3 deletions scripts/check-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,11 @@ function extractArgs(args, regexes) {
* For example: `superset-frontend/foo/bar.ts` -> `foo/bar.ts`
*
* @param {string[]} args
* @param {string} package
* @param {string} packageName
* @returns {string[]}
*/
function removePackageSegment(args, package) {
const packageSegment = package.concat(sep);
function removePackageSegment(args, packageName) {
const packageSegment = packageName.concat(sep);
return args.map((arg) => {
const normalizedPath = normalize(arg);

Expand Down
2 changes: 2 additions & 0 deletions superset-core/src/superset_core/semantic_layers/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ class Operator(str, enum.Enum):
NOT_IN = "NOT IN"
LIKE = "LIKE"
NOT_LIKE = "NOT LIKE"
ILIKE = "ILIKE"
NOT_ILIKE = "NOT ILIKE"
IS_NULL = "IS NULL"
IS_NOT_NULL = "IS NOT NULL"
ADHOC = "ADHOC"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "eslint-plugin-i18n-strings",
"name": "@superset-ui/eslint-plugin-i18n-strings",
"version": "1.0.0",
"description": "Warns about translation variables",
"keywords": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "eslint-plugin-icons",
"name": "@superset-ui/eslint-plugin-icons",
"version": "1.0.0",
"description": "Warns about direct usage of Ant Design icons",
"keywords": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "eslint-plugin-theme-colors",
"name": "@superset-ui/eslint-plugin-theme-colors",
"version": "1.0.0",
"description": "Warns about rgb(a)/hex/literal colors",
"keywords": [],
Expand Down
6 changes: 3 additions & 3 deletions superset-frontend/eslint.config.minimal.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
require('tsx/cjs');

const tsParser = require('@typescript-eslint/parser');
const themeColorsPlugin = require('eslint-plugin-theme-colors');
const iconsPlugin = require('eslint-plugin-icons');
const i18nStringsPlugin = require('eslint-plugin-i18n-strings');
const themeColorsPlugin = require('@superset-ui/eslint-plugin-theme-colors');
const iconsPlugin = require('@superset-ui/eslint-plugin-icons');
const i18nStringsPlugin = require('@superset-ui/eslint-plugin-i18n-strings');

module.exports = [
// Files this config applies to. Flat config has no `--ext`; globs live here.
Expand Down
33 changes: 18 additions & 15 deletions superset-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions superset-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@
"@storybook/addon-links": "10.5.10",
"@storybook/react-webpack5": "10.5.10",
"@storybook/test-runner": "0.24.4",
"@superset-ui/eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
"@superset-ui/eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"@superset-ui/eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.16.1",
"@swc/plugin-emotion": "^15.0.0",
Expand Down Expand Up @@ -303,8 +306,6 @@
"eslint": "^10.9.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest-dom": "^5.10.1",
"eslint-plugin-lodash": "^8.0.0",
Expand All @@ -313,7 +314,6 @@
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.2",
"eslint-plugin-storybook": "10.5.10",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"history": "^5.3.0",
Expand Down
6 changes: 6 additions & 0 deletions superset-frontend/packages/superset-core/src/theme/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,12 @@ export interface ThemeContextType {
canDetectOSPreference: () => boolean;
createDashboardThemeProvider: (themeId: string) => Promise<Theme | null>;
getAppliedThemeId: () => number | null;
/**
* Re-reads the persisted system default/dark themes from the server and
* re-applies them live, so changes made on the Themes admin page take effect
* without a full page reload.
*/
refreshSystemThemes: () => Promise<void>;
}

/**
Expand Down
Loading
Loading