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
23 changes: 20 additions & 3 deletions superset/commands/report/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def __init__(
self._scheduled_dttm = scheduled_dttm
self._start_dttm = datetime.utcnow()
self._execution_id = execution_id
self._filter_warnings: list[str] = []

def update_report_schedule_and_log(
self,
Expand Down Expand Up @@ -265,7 +266,11 @@ def get_dashboard_urls(
if (
dashboard_state := self._report_schedule.extra.get("dashboard")
) and feature_flag_manager.is_feature_enabled("ALERT_REPORT_TABS"):
native_filter_params = self._report_schedule.get_native_filters_params()
native_filter_params, filter_warnings = (
self._report_schedule.get_native_filters_params()
)
if filter_warnings:
self._filter_warnings.extend(filter_warnings)
if anchor := dashboard_state.get("anchor"):
try:
anchor_list: list[str] = json.loads(anchor)
Expand Down Expand Up @@ -831,7 +836,13 @@ def next(self) -> None: # noqa: C901
)
return
self.send()
self.update_report_schedule_and_log(ReportState.SUCCESS)
# Include filter warnings in the log if any were collected
warning_message = (
";".join(self._filter_warnings) if self._filter_warnings else None
)
self.update_report_schedule_and_log(
ReportState.SUCCESS, error_message=warning_message
)
except (SupersetErrorsException, Exception) as first_ex:
error_message = str(first_ex)
if isinstance(first_ex, SupersetErrorsException):
Expand Down Expand Up @@ -976,7 +987,13 @@ def next(self) -> None:

try:
self.send()
self.update_report_schedule_and_log(ReportState.SUCCESS)
# Include filter warnings in the log if any were collected
warning_message = (
";".join(self._filter_warnings) if self._filter_warnings else None
)
self.update_report_schedule_and_log(
ReportState.SUCCESS, error_message=warning_message
)
except Exception as ex: # pylint: disable=broad-except
try:
self.update_report_schedule_and_log(
Expand Down
39 changes: 32 additions & 7 deletions superset/reports/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# under the License.
"""A collection of ORM sqlalchemy models for Superset"""

import logging
from typing import Any, Optional

import prison
Expand Down Expand Up @@ -47,6 +48,8 @@
from superset.utils.backports import StrEnum
from superset.utils.core import MediumText

logger = logging.getLogger(__name__)

metadata = Model.metadata # pylint: disable=no-member


Expand Down Expand Up @@ -187,24 +190,46 @@ def __repr__(self) -> str:
def crontab_humanized(self) -> str:
return get_description(self.crontab)

def get_native_filters_params(self) -> str:
def get_native_filters_params(self) -> tuple[str, list[str]]:
"""
Generate native filter params for dashboard URL.

Returns:
A tuple of (rison_encoded_params, list_of_warning_messages).
Warnings are returned so they can be surfaced to users in the
execution log.
"""
params: dict[str, Any] = {}
warnings: list[str] = []
dashboard = self.extra.get("dashboard")
if dashboard and dashboard.get("nativeFilters"):
for filter in dashboard.get("nativeFilters") or []: # type: ignore
native_filters = dashboard.get("nativeFilters") or []
for native_filter in native_filters: # type: ignore
native_filter_id = native_filter.get("nativeFilterId")
filter_type = native_filter.get("filterType")
Comment on lines +206 to +209

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: nativeFilters is consumed as a list of dicts, but the code never validates that shape. If extra_json contains a non-list value (or list elements that are not dicts), calling .get() on native_filter will raise AttributeError and crash report execution. Validate the container and each item before accessing keys, and skip malformed entries with a warning. [type error]

Severity Level: Major ⚠️
- ❌ reports.execute task can fail for malformed nativeFilters.
- ❌ Dashboard tab screenshot flow aborts before notification delivery.
- ⚠️ Execution logs contain unexpected-error noise, not filter warnings.
Suggested change
native_filters = dashboard.get("nativeFilters") or []
for native_filter in native_filters: # type: ignore
native_filter_id = native_filter.get("nativeFilterId")
filter_type = native_filter.get("filterType")
native_filters = dashboard.get("nativeFilters") or []
if not isinstance(native_filters, list):
warning_msg = (
"Skipping malformed nativeFilters payload: expected list, "
f"got {type(native_filters).__name__}"
)
warnings.append(warning_msg)
logger.warning(warning_msg)
native_filters = []
for native_filter in native_filters:
if not isinstance(native_filter, dict):
warning_msg = (
"Skipping malformed native filter entry: expected object, "
f"got {type(native_filter).__name__}"
)
warnings.append(warning_msg)
logger.warning(warning_msg)
continue
native_filter_id = native_filter.get("nativeFilterId")
filter_type = native_filter.get("filterType")
Steps of Reproduction ✅
1. Create or update a report schedule through `ReportScheduleRestApi.post`
(`superset/reports/api.py:358`) with `extra.dashboard.nativeFilters` set to a non-list
(schema allows generic dict via `extra = fields.Dict` in
`superset/reports/schemas.py:232-234`).

2. Ensure the schedule is a dashboard report path with tabs enabled (`ALERT_REPORT_TABS`),
then trigger execution via Celery task `reports.execute`
(`superset/tasks/scheduler.py:117-135`), which runs `AsyncExecuteReportScheduleCommand`.

3. Execution reaches `BaseReportState.get_dashboard_urls`
(`superset/commands/report/execute.py:258-272`), which calls
`ReportSchedule.get_native_filters_params()` (`superset/reports/models.py:193`).

4. In `get_native_filters_params`, loop at `superset/reports/models.py:207` iterates
malformed entries and line `208` calls `native_filter.get(...)`; for non-dict items this
raises `AttributeError`, which bubbles to `ReportScheduleUnexpectedError`
(`superset/commands/report/execute.py:1090-1091`) and task failure handling
(`superset/tasks/scheduler.py:36-40`).
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/reports/models.py
**Line:** 206:209
**Comment:**
	*Type Error: `nativeFilters` is consumed as a list of dicts, but the code never validates that shape. If `extra_json` contains a non-list value (or list elements that are not dicts), calling `.get()` on `native_filter` will raise `AttributeError` and crash report execution. Validate the container and each item before accessing keys, and skip malformed entries with a warning.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎


if native_filter_id is None or filter_type is None:
warning_msg = (
f"Skipping malformed native filter missing required "
f"fields: {native_filter}"
)
warnings.append(warning_msg)
logger.warning(warning_msg)
continue
Comment on lines +211 to +218

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing required fields sounds like an error. Are we sure we want to skip these?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the requirement, waiting back on feedback to make sure we are aligned on the scope


params = {
**params,
**self._generate_native_filter(
filter["nativeFilterId"],
filter["filterType"],
filter["columnName"],
filter["filterValues"],
native_filter_id,
filter_type,
native_filter.get("columnName") or "",
native_filter.get("filterValues") or [],
),
Comment thread
alexandrusoare marked this conversation as resolved.
}
# hack(hughhh): workaround for escaping prison not handling quotes right
rison = prison.dumps(params)
rison = rison.replace("'", "%27")
return rison
return rison, warnings

def _generate_native_filter(
self,
Expand Down
8 changes: 8 additions & 0 deletions tests/unit_tests/commands/report/execute_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ def test_get_dashboard_urls_with_multiple_tabs(
"urlParams": None,
}
}
mock_report_schedule.get_native_filters_params.return_value = ( # type: ignore
"()",
[],
)

class_instance: BaseReportState = BaseReportState(
mock_report_schedule, "January 1, 2021", "execution_id_example"
Expand Down Expand Up @@ -327,6 +331,10 @@ def test_get_dashboard_urls_with_exporting_dashboard_only(
"urlParams": None,
}
}
mock_report_schedule.get_native_filters_params.return_value = ( # type: ignore
"()",
[],
)
mock_run.return_value = "url1"

class_instance: BaseReportState = BaseReportState(
Expand Down
121 changes: 97 additions & 24 deletions tests/unit_tests/reports/model_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import pytest

from superset.reports.models import ReportSchedule

Expand All @@ -37,9 +36,14 @@ def test_get_native_filters_params():
}
}

assert report_schedule.get_native_filters_params() == (
"(filter_id:(extraFormData:(filters:!((col:column_name,op:IN,val:!(value1,value2)))),filterState:(label:column_name,validateStatus:!f,value:!(value1,value2)),id:filter_id,ownState:()))"
result, warnings = report_schedule.get_native_filters_params()
expected = (
"(filter_id:(extraFormData:(filters:!((col:column_name,op:IN,"
"val:!(value1,value2)))),filterState:(label:column_name,"
"validateStatus:!f,value:!(value1,value2)),id:filter_id,ownState:()))"
)
assert result == expected
assert warnings == []


def test_get_native_filters_params_multiple_filters():
Expand All @@ -66,9 +70,17 @@ def test_get_native_filters_params_multiple_filters():
}
}

assert report_schedule.get_native_filters_params() == (
"(filter_id_1:(extraFormData:(filters:!((col:column_name_1,op:IN,val:!(value1,value2)))),filterState:(label:column_name_1,validateStatus:!f,value:!(value1,value2)),id:filter_id_1,ownState:()),filter_id_2:(extraFormData:(filters:!((col:column_name_2,op:IN,val:!(value3,value4)))),filterState:(label:column_name_2,validateStatus:!f,value:!(value3,value4)),id:filter_id_2,ownState:()))"
result, warnings = report_schedule.get_native_filters_params()
expected = (
"(filter_id_1:(extraFormData:(filters:!((col:column_name_1,op:IN,"
"val:!(value1,value2)))),filterState:(label:column_name_1,"
"validateStatus:!f,value:!(value1,value2)),id:filter_id_1,ownState:()),"
"filter_id_2:(extraFormData:(filters:!((col:column_name_2,op:IN,"
"val:!(value3,value4)))),filterState:(label:column_name_2,"
"validateStatus:!f,value:!(value3,value4)),id:filter_id_2,ownState:()))"
)
assert result == expected
assert warnings == []


def test_report_generate_native_filter_no_values():
Expand Down Expand Up @@ -99,9 +111,10 @@ def test_report_generate_native_filter_no_values():
}


def test_get_native_filters_params_invalid_structure():
def test_get_native_filters_params_missing_filter_values():
"""
Test the ``get_native_filters_params`` method with invalid structure.
Test the ``get_native_filters_params`` method with missing filterValues.
Should handle gracefully by using empty list as default.
"""
report_schedule = ReportSchedule()
report_schedule.extra = {
Expand All @@ -111,29 +124,85 @@ def test_get_native_filters_params_invalid_structure():
"nativeFilterId": "filter_id",
"columnName": "column_name",
"filterType": "filter_select",
# Missing "filterValues" key
# Missing "filterValues" key - should default to []
}
]
}
}

with pytest.raises(KeyError, match="'filterValues'"):
report_schedule.get_native_filters_params()
# Should not raise, should handle gracefully with empty filterValues
result, warnings = report_schedule.get_native_filters_params()
assert "filter_id" in result
assert "column_name" in result
assert warnings == []


# todo(hugh): how do we want to handle this case?
# def test_report_generate_native_filter_invalid_filter_id():
# """
# Test the ``_generate_native_filter`` method with invalid filter id.
# """
# report_schedule = ReportSchedule()
# native_filter_id = None
# column_name = "column_name"
# values = ["value1", "value2"]
def test_get_native_filters_params_explicit_none_values():
"""
Test the ``get_native_filters_params`` method with explicit None values.
Should handle gracefully by coercing None to empty string/list.
"""
report_schedule = ReportSchedule()
report_schedule.extra = {
"dashboard": {
"nativeFilters": [
{
"nativeFilterId": "filter_id",
"columnName": None, # Explicit None
"filterType": "filter_select",
"filterValues": None, # Explicit None
}
]
}
}

# Should not raise TypeError, should handle gracefully
result, warnings = report_schedule.get_native_filters_params()
assert "filter_id" in result
assert warnings == []


def test_get_native_filters_params_missing_required_fields():
"""
Test the ``get_native_filters_params`` method with missing required fields.
Filters missing nativeFilterId or filterType should be skipped.
"""
report_schedule = ReportSchedule()
report_schedule.extra = {
"dashboard": {
"nativeFilters": [
{
# Missing nativeFilterId - should be skipped
"filterType": "filter_select",
"columnName": "column_name",
"filterValues": ["value1"],
},
{
# Missing filterType - should be skipped
"nativeFilterId": "filter_2",
"columnName": "column_name",
"filterValues": ["value2"],
},
{
# Valid filter - should be processed
"nativeFilterId": "filter_3",
"filterType": "filter_select",
"columnName": "column_name",
"filterValues": ["value3"],
},
]
}
}

# assert report_schedule._generate_native_filter(
# native_filter_id, column_name, values
# ) == {}
result, warnings = report_schedule.get_native_filters_params()
# Only the valid filter should be in the result
assert "filter_3" in result
assert "filter_2" not in result
assert "value1" not in result
assert "value3" in result
# Two malformed filters should generate two warnings
assert len(warnings) == 2
assert all("Skipping malformed native filter" in w for w in warnings)


def test_report_generate_native_filter():
Expand Down Expand Up @@ -173,7 +242,9 @@ def test_get_native_filters_params_empty():
report_schedule = ReportSchedule()
report_schedule.extra = {}

assert report_schedule.get_native_filters_params() == "()"
result, warnings = report_schedule.get_native_filters_params()
assert result == "()"
assert warnings == []


def test_get_native_filters_params_no_native_filters():
Expand All @@ -183,7 +254,9 @@ def test_get_native_filters_params_no_native_filters():
report_schedule = ReportSchedule()
report_schedule.extra = {"dashboard": {"nativeFilters": []}}

assert report_schedule.get_native_filters_params() == "()"
result, warnings = report_schedule.get_native_filters_params()
assert result == "()"
assert warnings == []


def test_report_generate_native_filter_empty_values():
Expand Down
Loading