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
13 changes: 13 additions & 0 deletions .github/workflows/check-python-deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ jobs:
if: steps.check.outputs.python
uses: ./.github/actions/setup-backend/

# Authenticate the Docker daemon so the python:slim pull in
# uv-pip-compile.sh uses our (much higher) authenticated rate limit
# instead of the shared-runner anonymous one. Best-effort: on fork PRs the
# secrets are unavailable, so this no-ops and the pull falls back to
# anonymous (covered by the retry loop in the script).
- name: Login to Docker Hub
if: steps.check.outputs.python
continue-on-error: true
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Run uv
if: steps.check.outputs.python
run: ./scripts/uv-pip-compile.sh
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/superset-python-integrationtest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ jobs:
services:
mysql:
image: mysql:8.0
# Authenticated pulls use our higher Docker Hub rate limit. Empty on
# fork PRs (secrets unavailable) -> runner falls back to anonymous.
credentials:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
MYSQL_ROOT_PASSWORD: root
ports:
Expand All @@ -38,6 +43,9 @@ jobs:
--health-retries=5
redis:
image: redis:7-alpine
credentials:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
options: --entrypoint redis-server
ports:
- 16379:6379
Expand Down Expand Up @@ -121,6 +129,9 @@ jobs:
services:
postgres:
image: postgres:17-alpine
credentials:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
Expand All @@ -130,6 +141,9 @@ jobs:
- 15432:5432
redis:
image: redis:7-alpine
credentials:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
ports:
- 16379:6379
steps:
Expand Down Expand Up @@ -186,6 +200,9 @@ jobs:
services:
redis:
image: redis:7-alpine
credentials:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
ports:
- 16379:6379
steps:
Expand Down
18 changes: 17 additions & 1 deletion docker/docker-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,23 @@ case "${1}" in
;;
app)
echo "Starting web app (using development server)..."
flask run -p $PORT --reload --debugger --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"

# Environment-based debugger control for security
# Only enable Werkzeug interactive debugger when explicitly requested
# Modern Werkzeug (3.0+) includes PIN protection, but defense-in-depth approach
# Override FLASK_DEBUG so the effective state matches SUPERSET_DEBUG_ENABLED even
# when FLASK_DEBUG=true is inherited from docker/.env or .flaskenv
if [[ "${SUPERSET_DEBUG_ENABLED:-}" == "true" ]]; then
export FLASK_DEBUG=1
DEBUGGER_FLAG="--debugger"
echo " ⚠️ Werkzeug debugger enabled (requires PIN for /console access)"
else
export FLASK_DEBUG=0
DEBUGGER_FLAG="--no-debugger"
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
fi

flask run -p $PORT --reload $DEBUGGER_FLAG --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"
;;
app-gunicorn)
echo "Starting web app..."
Expand Down
9 changes: 8 additions & 1 deletion docs/admin_docs/installation/pypi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,15 @@ superset load_examples
superset init

# To start a development web server on port 8088, use -p to bind to another port
superset run -p 8088 --with-threads --reload --debugger
superset run -p 8088 --with-threads --reload

# For debugging with interactive console (⚠️ localhost only)
# superset run -p 8088 --with-threads --reload --debugger
```

:::warning Security Note
The `--debugger` flag enables Werkzeug's interactive console at `/console`. Only use this for local development and never bind to `0.0.0.0` or expose the server to networks when debugging is enabled.
:::

If everything worked, you should be able to navigate to `hostname:port` in your browser (e.g.
locally by default at `localhost:8088`) and login using the username and password you created.
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,15 @@ superset load_examples
superset init

# To start a development web server on port 8088, use -p to bind to another port
superset run -p 8088 --with-threads --reload --debugger
superset run -p 8088 --with-threads --reload

# For debugging with interactive console (⚠️ localhost only)
# superset run -p 8088 --with-threads --reload --debugger
```

:::warning Security Note
The `--debugger` flag enables Werkzeug's interactive console at `/console`. Only use this for local development and never bind to `0.0.0.0` or expose the server to networks when debugging is enabled.
:::

If everything worked, you should be able to navigate to `hostname:port` in your browser (e.g.
locally by default at `localhost:8088`) and login using the username and password you created.
2 changes: 2 additions & 0 deletions docs/developer_docs/contributing/development-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ Affecting the Docker build process:
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`
- **SUPERSET_LOG_LEVEL (default=info)**: Can be set to debug, info, warning, error, critical
for more verbose logging
- **SUPERSET_DEBUG_ENABLED (default=false)**: Enable Werkzeug debugger with interactive console.
Set to `true` for debugging: `SUPERSET_DEBUG_ENABLED=true docker compose up`

For more env vars that affect your configuration, see this
[superset_config.py](https://github.com/apache/superset/blob/master/docker/pythonpath_dev/superset_config.py)
Expand Down
23 changes: 22 additions & 1 deletion scripts/uv-pip-compile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,32 @@ if [ -z "$RUNNING_IN_DOCKER" ]; then

echo "Running in Docker (Python ${PYTHON_VERSION} on Linux)..."

IMAGE="python:${PYTHON_VERSION}-slim"

# Pre-pull the image with a few retries to absorb transient Docker Hub
# registry failures ("context deadline exceeded" / anonymous rate-limit blips
# on shared CI runners). Without this a flaky pull fails the whole
# check-python-deps job on an infrastructure hiccup rather than a real
# dependency drift. The pull is in the `until` condition so `set -e` does not
# abort on an individual failed attempt.
attempt=1
max_attempts=4
until docker pull "$IMAGE"; do
if [ "$attempt" -ge "$max_attempts" ]; then
echo "docker pull $IMAGE failed after ${max_attempts} attempts" >&2
exit 1
fi
delay=$((attempt * 10))
echo "docker pull $IMAGE failed (attempt ${attempt}/${max_attempts}); retrying in ${delay}s..." >&2
sleep "$delay"
attempt=$((attempt + 1))
done

docker run --rm \
-v "$(pwd)":/app \
-w /app \
-e RUNNING_IN_DOCKER=1 \
python:${PYTHON_VERSION}-slim \
"$IMAGE" \
bash -c "pip install uv && ./scripts/uv-pip-compile.sh $*"

exit $?
Expand Down
2 changes: 1 addition & 1 deletion superset-frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const restrictedImportsRules = {
'no-jest-mock-console': {
name: 'jest-mock-console',
message: 'Please use native Jest spies, i.e. jest.spyOn(console, "warn")',
}
},
};

module.exports = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,7 @@ describe('BigNumberWithTrendline transformProps', () => {
height: 300,
queriesData: [
{
data: [
{ __timestamp: 1, value: 100 },
] as unknown as BigNumberDatum[],
data: [{ __timestamp: 1, value: 100 }] as unknown as BigNumberDatum[],
colnames: ['__timestamp', 'value'],
coltypes: ['TEMPORAL', 'NUMERIC'],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,11 @@ function Echart(
// setOption(notMerge:true) replaces the dataZoom config, dropping any
// range the user has engaged. Preserve it across the call.
const previousZoom = notMerge
? (chartRef.current?.getOption() as { dataZoom?: DataZoomComponentOption[] })
?.dataZoom
? (
chartRef.current?.getOption() as {
dataZoom?: DataZoomComponentOption[];
}
)?.dataZoom
: undefined;
chartRef.current?.setOption(themedEchartOptions, {
notMerge,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ function CollectionControl({
// Two items can collide when keyAccessor returns falsy and the index
// fallback is used — breaking dnd-kit reordering and React reconciliation.
// Assign a stable nanoid per item ref when no key is available.
const generatedIdsRef = useRef<WeakMap<CollectionItem, string>>(new WeakMap());
const generatedIdsRef = useRef<WeakMap<CollectionItem, string>>(
new WeakMap(),
);
const itemIds = useMemo(
() =>
value.map(item => {
Expand Down
11 changes: 7 additions & 4 deletions superset/charts/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from superset.commands.chart.create import CreateChartCommand
from superset.commands.chart.delete import DeleteChartCommand
from superset.commands.chart.exceptions import (
ChartAccessDeniedError,
ChartCreateFailedError,
ChartDeleteFailedError,
ChartForbiddenError,
Expand Down Expand Up @@ -440,6 +441,8 @@ def put(self, pk: int) -> Response:
response = self.response_404()
except ChartForbiddenError:
response = self.response_403()
except DashboardsForbiddenError as ex:
response = self.response(ex.status, message=ex.message)
except TagForbiddenError as ex:
response = self.response(403, message=str(ex))
except ChartInvalidError as ex:
Expand Down Expand Up @@ -972,7 +975,7 @@ def add_favorite(self, pk: int) -> Response:
AddFavoriteChartCommand(pk).run()
except ChartNotFoundError:
return self.response_404()
except ChartForbiddenError:
except (ChartAccessDeniedError, ChartForbiddenError):
return self.response_403()

return self.response(200, result="OK")
Expand Down Expand Up @@ -1017,9 +1020,9 @@ def remove_favorite(self, pk: int) -> Response:
try:
DelFavoriteChartCommand(pk).run()
except ChartNotFoundError:
self.response_404()
except ChartForbiddenError:
self.response_403()
return self.response_404()
except (ChartAccessDeniedError, ChartForbiddenError):
return self.response_403()

return self.response(200, result="OK")

Expand Down
7 changes: 7 additions & 0 deletions superset/charts/data/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,13 @@ def _get_data_response(
def _extract_export_params_from_request(self) -> tuple[str | None, int | None]:
"""Extract filename and expected_rows from request for streaming exports."""
filename = request.form.get("filename")
if filename:
# Sanitize the user-supplied filename before it is used in the
# Content-Disposition header (consistent with the generated-name
# path). secure_filename may reduce a name consisting entirely of
# unsupported characters to an empty string, in which case fall back
# to the generated default downstream.
filename = secure_filename(filename) or None
if filename:
logger.info("FRONTEND PROVIDED FILENAME: %s", filename)

Expand Down
5 changes: 5 additions & 0 deletions superset/commands/chart/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@
from superset.commands.base import BaseCommand, CreateMixin
from superset.commands.chart.exceptions import (
ChartCreateFailedError,
ChartForbiddenError,
ChartInvalidError,
DashboardsForbiddenError,
DashboardsNotFoundValidationError,
)
from superset.commands.utils import get_datasource_by_id
from superset.daos.chart import ChartDAO
from superset.daos.dashboard import DashboardDAO
from superset.exceptions import SupersetSecurityException
from superset.utils import json
from superset.utils.decorators import on_error, transaction

Expand Down Expand Up @@ -69,6 +71,9 @@ def validate(self) -> None:
try:
datasource = get_datasource_by_id(datasource_id, datasource_type)
self._properties["datasource_name"] = datasource.name
security_manager.raise_for_access(datasource=datasource)
except SupersetSecurityException as ex:
raise ChartForbiddenError() from ex
except ValidationError as ex:
exceptions.append(ex)

Expand Down
8 changes: 7 additions & 1 deletion superset/commands/chart/fave.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
import logging
from functools import partial

from superset import security_manager
from superset.commands.base import BaseCommand
from superset.commands.chart.exceptions import (
ChartAccessDeniedError,
ChartFaveError,
ChartNotFoundError,
)
from superset.daos.chart import ChartDAO
from superset.exceptions import SupersetSecurityException
from superset.models.slice import Slice
from superset.utils.decorators import on_error, transaction

Expand All @@ -44,5 +47,8 @@ def validate(self) -> None:
chart = ChartDAO.find_by_id(self._chart_id)
if not chart:
raise ChartNotFoundError()

try:
security_manager.raise_for_access(chart=chart)
except SupersetSecurityException as ex:
raise ChartAccessDeniedError() from ex
self._chart = chart
8 changes: 7 additions & 1 deletion superset/commands/chart/unfave.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
import logging
from functools import partial

from superset import security_manager
from superset.commands.base import BaseCommand
from superset.commands.chart.exceptions import (
ChartAccessDeniedError,
ChartNotFoundError,
ChartUnfaveError,
)
from superset.daos.chart import ChartDAO
from superset.exceptions import SupersetSecurityException
from superset.models.slice import Slice
from superset.utils.decorators import on_error, transaction

Expand All @@ -44,5 +47,8 @@ def validate(self) -> None:
chart = ChartDAO.find_by_id(self._chart_id)
if not chart:
raise ChartNotFoundError()

try:
security_manager.raise_for_access(chart=chart)
except SupersetSecurityException as ex:
raise ChartAccessDeniedError() from ex
self._chart = chart
Loading
Loading