Compare commits

...
Author SHA1 Message Date
Elizabeth Thompson 1309b41da2 fix(tags): also catch SupersetParseError when validating tagged query access
Widen the access-validation carve-out to SupersetParseError, a sibling of
SupersetSecurityException under SupersetErrorException that process_jinja_sql
raises for unresolvable partition macros. It previously slipped past the
(SupersetSecurityException, TemplateError) tuple and surfaced as exactly the
unhandled 500 this PR set out to eliminate.

Split the handler into two arms: security denials stay silent (routine), while
TemplateError/SupersetParseError are logged for server-side visibility and their
error text is preserved in the surfaced message rather than discarded.

Add a regression test for the SupersetParseError path (verified to fail before
this change).
2026-08-25 01:22:17 +00:00
Elizabeth ThompsonandClaude Opus 4.8 893925f0eb test(tags): make TemplateError regression test hermetic, no DB dependency
The regression test let the real security_manager.raise_for_access(query=...)
run, which opens a live DB connection to introspect table-level perms before
reaching the Jinja parse. That only passed locally because a DB was up; the
unit_tests CI sandbox has no Postgres, so it failed there.

Mock raise_for_access directly to raise TemplateError instead. This still
proves what matters: except (SupersetSecurityException, TemplateError) in
create.py catches it and surfaces as TagInvalidError rather than escaping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 17:16:37 +00:00
Elizabeth ThompsonandClaude Opus 4.8 698181e93d fix(tags): catch TemplateError when validating access for tagged SQL Lab queries
When tagging a saved SQL Lab query, CreateCustomTagCommand._validate_object_access
calls security_manager.raise_for_access(query=...). For a user relying on
per-table/dataset permissions (no blanket database access), that path parses the
query's Jinja-templated SQL via process_jinja_sql(), which can raise a raw
jinja2 TemplateError (e.g. TemplateSyntaxError on malformed Jinja). The narrow
`except SupersetSecurityException:` let it escape as an unhandled 500. Widen the
except to also catch TemplateError so it surfaces as a validation error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 16:46:50 +00:00
2 changed files with 95 additions and 2 deletions
+26 -2
View File
@@ -18,6 +18,8 @@ import logging
from functools import partial
from typing import Any
from jinja2.exceptions import TemplateError
from superset import security_manager
from superset.commands.base import BaseCommand, CreateMixin
from superset.commands.tag.exceptions import TagCreateFailedError, TagInvalidError
@@ -27,7 +29,7 @@ from superset.commands.tag.utils import (
to_object_type,
)
from superset.daos.tag import TagDAO
from superset.exceptions import SupersetSecurityException
from superset.exceptions import SupersetParseError, SupersetSecurityException
from superset.tags.models import ObjectType, TagType
from superset.utils.decorators import on_error, transaction
@@ -98,8 +100,30 @@ class CreateCustomTagCommand(CreateMixin, BaseCommand):
)
)
except SupersetSecurityException:
# A routine, expected authorization denial; swallowed silently by
# design (no logging) and surfaced to the caller as a validation
# failure rather than an unhandled 500.
exceptions.append(
TagCreateFailedError(f"Access denied for {object_type} {object_id}")
TagCreateFailedError(
f"Could not validate access for {object_type} {object_id}"
)
)
except (TemplateError, SupersetParseError) as ex:
# Authorizing a saved query parses its Jinja-templated SQL to resolve
# table references. Malformed Jinja (TemplateError) or an
# unresolvable partition macro (SupersetParseError) is a validation
# failure, not an unhandled 500 -- but unlike an access denial it is
# genuinely unexpected, so log it for server-side visibility and
# preserve the underlying error text instead of discarding it.
logger.warning(
"Could not parse query %s while validating tag access: %s",
object_id,
str(ex),
)
exceptions.append(
TagCreateFailedError(
f"Could not validate access for {object_type} {object_id}: {ex}"
)
)
@@ -108,6 +108,75 @@ def test_create_command_success(session_with_data: Session, mocker: MockerFixtur
)
def test_validate_object_access_query_malformed_jinja(
session_with_data: Session, mocker: MockerFixture
):
"""A saved query whose Jinja-templated SQL fails to parse during access
checks must surface as a validation error, not an unhandled
``jinja2.TemplateError`` escaping as a 500.
When ``raise_for_access(query=...)`` authorizes a saved query via
per-table permissions it parses the query's Jinja SQL (e.g. an unclosed
``{% if %}`` block raises ``TemplateSyntaxError``). Mock that call to raise
the ``TemplateError`` directly so the test stays hermetic and does not open
a live DB connection to introspect table-level perms.
"""
from jinja2.exceptions import TemplateError
from superset.commands.tag.create import CreateCustomTagCommand
from superset.commands.tag.exceptions import TagInvalidError
from superset.models.sql_lab import SavedQuery
from superset.tags.models import ObjectType
query = db.session.query(SavedQuery).first()
mocker.patch("superset.commands.tag.create.to_object_model", return_value=query)
mocker.patch(
"superset.commands.tag.create.security_manager.raise_for_access",
side_effect=TemplateError("unclosed {% if %}"),
)
command = CreateCustomTagCommand(ObjectType.query, query.id, ["tag"])
with pytest.raises(TagInvalidError):
command.validate()
def test_validate_object_access_query_unresolvable_partition_macro(
session_with_data: Session, mocker: MockerFixture
):
"""A saved query whose partition macro cannot be resolved statically raises
``SupersetParseError`` during access checks. Like ``TemplateError``, it is a
sibling of ``SupersetSecurityException`` under ``SupersetErrorException`` and
would otherwise escape as an unhandled 500, so it must also surface as a
validation error.
Mock ``raise_for_access`` to raise the error directly so the test stays
hermetic and does not open a live DB connection to introspect table perms.
"""
from superset.commands.tag.create import CreateCustomTagCommand
from superset.commands.tag.exceptions import TagInvalidError
from superset.exceptions import SupersetParseError
from superset.models.sql_lab import SavedQuery
from superset.tags.models import ObjectType
query = db.session.query(SavedQuery).first()
mocker.patch("superset.commands.tag.create.to_object_model", return_value=query)
mocker.patch(
"superset.commands.tag.create.security_manager.raise_for_access",
side_effect=SupersetParseError(
sql="select * from {{ latest_partition('foo') }}",
message="Unresolvable partition macro",
),
)
command = CreateCustomTagCommand(ObjectType.query, query.id, ["tag"])
with pytest.raises(TagInvalidError):
command.validate()
def test_create_command_success_clear(
session_with_data: Session, mocker: MockerFixture
):