From b4fac82c39d2e59aa1c0eae2e4d408e83bf3e59c Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Mon, 24 Aug 2026 16:41:44 +0000 Subject: [PATCH] fix(explore): catch TemplateError when validating access for query-backed form_data check_query_access() calls raise_for_access(query=query), which Jinja-renders the query's SQL to resolve table references. A malformed template surfaces as a raw jinja2.exceptions.TemplateError instead of a Superset exception, leaking as an opaque 500 from the explore form_data endpoints (used by the chart Explore/Drill-by cache) whenever datasource_type=query. Wrap the call and re-raise as the existing SupersetTemplateException (422), matching the same conversion already used in datasets/api.py, and map it to a proper response in ExploreFormDataRestApi's four handlers. --- superset/explore/form_data/api.py | 9 +++++++++ superset/explore/utils.py | 11 ++++++++++- tests/unit_tests/explore/utils_test.py | 25 ++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/superset/explore/form_data/api.py b/superset/explore/form_data/api.py index 6c882d92a6f..d8c1c3bfd32 100644 --- a/superset/explore/form_data/api.py +++ b/superset/explore/form_data/api.py @@ -30,6 +30,7 @@ from superset.commands.temporary_cache.exceptions import ( TemporaryCacheResourceNotFoundError, ) from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP +from superset.exceptions import SupersetTemplateException from superset.explore.form_data.schemas import FormDataPostSchema, FormDataPutSchema from superset.extensions import event_logger from superset.views.base_api import BaseSupersetApi, requires_json, statsd_metrics @@ -110,6 +111,8 @@ class ExploreFormDataRestApi(BaseSupersetApi): return self.response(403, message=str(ex)) except TemporaryCacheResourceNotFoundError as ex: return self.response(404, message=str(ex)) + except SupersetTemplateException as ex: + return self.response(ex.status, message=str(ex)) @expose("/form_data/", methods=("PUT",)) @protect() @@ -183,6 +186,8 @@ class ExploreFormDataRestApi(BaseSupersetApi): return self.response(403, message=str(ex)) except TemporaryCacheResourceNotFoundError as ex: return self.response(404, message=str(ex)) + except SupersetTemplateException as ex: + return self.response(ex.status, message=str(ex)) @expose("/form_data/", methods=("GET",)) @protect() @@ -234,6 +239,8 @@ class ExploreFormDataRestApi(BaseSupersetApi): return self.response(403, message=str(ex)) except TemporaryCacheResourceNotFoundError as ex: return self.response(404, message=str(ex)) + except SupersetTemplateException as ex: + return self.response(ex.status, message=str(ex)) @expose("/form_data/", methods=("DELETE",)) @protect() @@ -286,3 +293,5 @@ class ExploreFormDataRestApi(BaseSupersetApi): return self.response(403, message=str(ex)) except TemporaryCacheResourceNotFoundError as ex: return self.response(404, message=str(ex)) + except SupersetTemplateException as ex: + return self.response(ex.status, message=str(ex)) diff --git a/superset/explore/utils.py b/superset/explore/utils.py index 28e4db2b916..0535bbc01fb 100644 --- a/superset/explore/utils.py +++ b/superset/explore/utils.py @@ -16,6 +16,8 @@ # under the License. from typing import Optional +from jinja2.exceptions import TemplateError + from superset import security_manager from superset.commands.chart.exceptions import ( ChartAccessDeniedError, @@ -33,6 +35,7 @@ from superset.commands.exceptions import ( from superset.daos.chart import ChartDAO from superset.daos.dataset import DatasetDAO from superset.daos.query import QueryDAO +from superset.exceptions import SupersetTemplateException from superset.utils.core import DatasourceType @@ -53,7 +56,13 @@ def check_query_access(query_id: int) -> Optional[bool]: # Access checks below, no need to validate them twice as they can be expensive. query = QueryDAO.find_by_id(query_id, skip_base_filter=True) if query: - security_manager.raise_for_access(query=query) + try: + security_manager.raise_for_access(query=query) + except TemplateError as ex: + # raise_for_access() Jinja-renders the query's SQL to resolve + # the tables it touches; a malformed template surfaces here as + # a raw jinja2 exception rather than a Superset one. + raise SupersetTemplateException(str(ex)) from ex return True raise QueryNotFoundValidationError() diff --git a/tests/unit_tests/explore/utils_test.py b/tests/unit_tests/explore/utils_test.py index 54555d2814e..c7778a7f0dc 100644 --- a/tests/unit_tests/explore/utils_test.py +++ b/tests/unit_tests/explore/utils_test.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. from flask_appbuilder.security.sqla.models import User +from jinja2.exceptions import TemplateSyntaxError from pytest import raises # noqa: PT013 from pytest_mock import MockerFixture @@ -30,7 +31,7 @@ from superset.commands.exceptions import ( DatasourceNotFoundValidationError, QueryNotFoundValidationError, ) -from superset.exceptions import SupersetSecurityException +from superset.exceptions import SupersetSecurityException, SupersetTemplateException from superset.utils.core import DatasourceType, override_user dataset_find_by_id = "superset.daos.dataset.DatasetDAO.find_by_id" @@ -340,6 +341,28 @@ def test_query_has_access(mocker: MockerFixture) -> None: ) +def test_query_malformed_jinja_template(mocker: MockerFixture) -> None: + """ + ``raise_for_access(query=...)`` Jinja-renders the query's SQL to resolve + the tables it touches. A malformed template must surface as a + ``SupersetTemplateException``, not the raw ``jinja2`` exception. + """ + from superset.explore.utils import check_datasource_access + from superset.models.sql_lab import Query + + mocker.patch(query_find_by_id, return_value=Query()) + mocker.patch( + raise_for_access, + side_effect=TemplateSyntaxError("unexpected end of template", lineno=1), + ) + + with raises(SupersetTemplateException): # noqa: PT012 + check_datasource_access( + datasource_id=1, + datasource_type=DatasourceType.QUERY, + ) + + def test_query_no_access(mocker: MockerFixture, client) -> None: from superset.connectors.sqla.models import SqlaTable from superset.explore.utils import check_datasource_access