mirror of
https://github.com/apache/superset.git
synced 2026-05-19 06:45:15 +00:00
Compare commits
1 Commits
fix/mcp-ex
...
ce/task-10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b03642df1d |
@@ -26,7 +26,12 @@ from zipfile import is_zipfile, ZipFile
|
|||||||
from flask import request, Response, send_file
|
from flask import request, Response, send_file
|
||||||
from flask_appbuilder.api import expose, protect, rison as parse_rison, safe
|
from flask_appbuilder.api import expose, protect, rison as parse_rison, safe
|
||||||
from flask_appbuilder.api.schemas import get_item_schema
|
from flask_appbuilder.api.schemas import get_item_schema
|
||||||
from flask_appbuilder.const import API_RESULT_RES_KEY, API_SELECT_COLUMNS_RIS_KEY
|
from flask_appbuilder.const import (
|
||||||
|
API_FILTERS_RIS_KEY,
|
||||||
|
API_RESULT_RES_KEY,
|
||||||
|
API_SELECT_COLUMNS_RIS_KEY,
|
||||||
|
)
|
||||||
|
from flask_appbuilder.models.filters import Filters
|
||||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||||
from flask_babel import ngettext
|
from flask_babel import ngettext
|
||||||
from jinja2.exceptions import TemplateSyntaxError
|
from jinja2.exceptions import TemplateSyntaxError
|
||||||
@@ -307,6 +312,30 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||||||
list_outer_default_load = True
|
list_outer_default_load = True
|
||||||
show_outer_default_load = True
|
show_outer_default_load = True
|
||||||
|
|
||||||
|
def _handle_filters_args(self, rison_args: dict[str, Any]) -> Filters:
|
||||||
|
"""
|
||||||
|
Build a request-scoped ``Filters`` instance from Rison-encoded args.
|
||||||
|
|
||||||
|
Overrides :meth:`flask_appbuilder.api.ModelRestApi._handle_filters_args`,
|
||||||
|
which mutates ``self._filters`` (a single instance shared across requests).
|
||||||
|
Under concurrent traffic that shared state can leak filters from one
|
||||||
|
request into another — e.g. two parallel ``GET /api/v1/dataset/`` calls
|
||||||
|
filtering by different ``table_name`` values can return mixed results.
|
||||||
|
|
||||||
|
Returning a fresh ``Filters`` per call keeps each request isolated.
|
||||||
|
|
||||||
|
:param rison_args: Arguments parsed from the API request's Rison-encoded
|
||||||
|
``q`` parameter.
|
||||||
|
:returns: A request-scoped ``Filters`` instance joined with the API's
|
||||||
|
base filters.
|
||||||
|
"""
|
||||||
|
filters = self.datamodel.get_filters(
|
||||||
|
search_columns=self.search_columns,
|
||||||
|
search_filters=self.search_filters,
|
||||||
|
)
|
||||||
|
filters.rest_add_filters(rison_args.get(API_FILTERS_RIS_KEY, []))
|
||||||
|
return filters.get_joined_filters(self._base_filters)
|
||||||
|
|
||||||
@expose("/", methods=("POST",))
|
@expose("/", methods=("POST",))
|
||||||
@protect()
|
@protect()
|
||||||
@safe
|
@safe
|
||||||
|
|||||||
@@ -120,3 +120,41 @@ def test_get_dataset_include_rendered_sql_passes_table_to_template_processor(
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
mock_get_processor.assert_called_once_with(database=database, table=dataset)
|
mock_get_processor.assert_called_once_with(database=database, table=dataset)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_filters_args_returns_request_scoped_filters(
|
||||||
|
session: Session,
|
||||||
|
client: Any,
|
||||||
|
full_api_access: None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Dataset API: ``_handle_filters_args`` must return a fresh ``Filters``
|
||||||
|
instance per call so concurrent requests don't share filter state.
|
||||||
|
|
||||||
|
Regression test for #33828: under concurrent traffic the FAB default
|
||||||
|
implementation mutates ``self._filters`` (a single shared instance),
|
||||||
|
causing filters from one request to leak into another.
|
||||||
|
"""
|
||||||
|
from flask_appbuilder.const import API_FILTERS_RIS_KEY
|
||||||
|
|
||||||
|
from superset.datasets.api import DatasetRestApi
|
||||||
|
|
||||||
|
api = DatasetRestApi()
|
||||||
|
api.datamodel = MagicMock()
|
||||||
|
api.search_columns = ["table_name"]
|
||||||
|
api.search_filters = {}
|
||||||
|
api._base_filters = MagicMock() # noqa: SLF001
|
||||||
|
|
||||||
|
# Each call should construct a fresh Filters instance via datamodel.get_filters
|
||||||
|
rison_args = {
|
||||||
|
API_FILTERS_RIS_KEY: [{"col": "table_name", "opr": "eq", "value": "a"}],
|
||||||
|
}
|
||||||
|
api._handle_filters_args(rison_args) # noqa: SLF001
|
||||||
|
api._handle_filters_args(rison_args) # noqa: SLF001
|
||||||
|
|
||||||
|
assert api.datamodel.get_filters.call_count == 2
|
||||||
|
# Returned object must be the joined-filters result of the *fresh* Filters,
|
||||||
|
# not the shared self._filters attribute.
|
||||||
|
fresh_filters = api.datamodel.get_filters.return_value
|
||||||
|
assert fresh_filters.rest_add_filters.call_count == 2
|
||||||
|
assert fresh_filters.get_joined_filters.call_count == 2
|
||||||
|
|||||||
Reference in New Issue
Block a user