Compare commits

...
Author SHA1 Message Date
Elizabeth ThompsonandClaude Opus 4.8 58fa19bf90 fix(importers): catch JSONDecodeError in load_configs masked_encrypted_extra merge
load_configs() parses each config's `masked_encrypted_extra` field with
json.loads() before schema validation runs, in order to merge caller-supplied
`encrypted_extra_secrets` into it. That field comes straight from user-uploaded
import YAML, so a malformed value raised a raw simplejson.JSONDecodeError (a
ValueError, not a marshmallow.ValidationError). The enclosing except only
catches ValidationError, so the decode error escaped uncaught out of
ImportModelsCommand.validate() and surfaced as an opaque 500 instead of the
structured 422 every other per-file validation failure produces.

Add a sibling `except json.JSONDecodeError` clause that converts the decode
error into a ValidationError with the same {file_name: {field: [msg]}} shape,
so it flows into the existing exceptions list and downstream
CommandInvalidError aggregation. Additive catch only; existing success paths
are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 16:47:55 +00:00
2 changed files with 116 additions and 0 deletions
+14
View File
@@ -247,6 +247,20 @@ def load_configs(
)
exc.messages = {file_name: exc.messages}
exceptions.append(exc)
except json.JSONDecodeError as exc:
# masked_encrypted_extra comes straight from the imported YAML
# (before schema validation) and may not be valid JSON. Convert
# the raw decode error into a ValidationError so it flows into
# the aggregated CommandInvalidError like every other per-file
# validation failure, instead of escaping as an opaque 500.
logger.error(
"Invalid JSON in masked_encrypted_extra for %s: %s",
file_name,
exc,
)
exceptions.append(
ValidationError({file_name: {"masked_encrypted_extra": [str(exc)]}})
)
return configs
@@ -138,3 +138,105 @@ class TestLoadYaml:
with pytest.raises(ValidationError):
load_yaml("test.yaml", 'key: "unterminated string')
class TestLoadConfigs:
"""
load_configs() merges caller-supplied ``encrypted_extra_secrets`` into the
``masked_encrypted_extra`` field of each config, which comes straight from
the imported YAML (before schema validation). A malformed value there used
to raise a raw simplejson.JSONDecodeError that escaped uncaught (opaque
500); it must instead be collected as a ValidationError like every other
per-file failure.
"""
@staticmethod
def _trivial_schema(): # type: ignore[no-untyped-def]
from marshmallow import EXCLUDE, Schema
class TrivialSchema(Schema):
class Meta:
unknown = EXCLUDE
return TrivialSchema()
@patch("superset.commands.importers.v1.utils.db")
def test_invalid_json_in_masked_encrypted_extra_is_collected(
self, mock_db: object
) -> None:
"""A non-JSON ``masked_encrypted_extra`` is converted into a
ValidationError appended to ``exceptions`` rather than raising."""
from marshmallow.exceptions import ValidationError
from superset.commands.importers.v1.utils import load_configs
# No existing databases / ssh tunnels in the (mocked) metadata DB.
mock_db.session.query.return_value.all.return_value = [] # type: ignore[attr-defined]
file_name = "databases/db.yaml"
contents = {
file_name: (
"uuid: abc-123\n"
"password: secret\n"
"masked_encrypted_extra: not valid json\n"
)
}
exceptions: list[ValidationError] = []
configs = load_configs(
contents=contents,
schemas={"databases/": self._trivial_schema()},
passwords={},
exceptions=exceptions,
ssh_tunnel_passwords={},
ssh_tunnel_private_keys={},
ssh_tunnel_priv_key_passwords={},
encrypted_extra_secrets={file_name: {"$.foo": "actual_secret"}},
)
# The bad file is not added to configs, and a structured error is
# collected instead of a raw JSONDecodeError propagating out.
assert file_name not in configs
assert len(exceptions) == 1
assert isinstance(exceptions[0], ValidationError)
assert file_name in exceptions[0].messages
assert "masked_encrypted_extra" in exceptions[0].messages[file_name]
@patch("superset.commands.importers.v1.utils.db")
def test_valid_json_in_masked_encrypted_extra_still_merges(
self, mock_db: object
) -> None:
"""Control: valid JSON in ``masked_encrypted_extra`` still has the
secrets merged in and produces no exceptions."""
from marshmallow.exceptions import ValidationError
from superset.commands.importers.v1.utils import load_configs
from superset.utils import json
mock_db.session.query.return_value.all.return_value = [] # type: ignore[attr-defined]
file_name = "databases/db.yaml"
contents = {
file_name: (
"uuid: abc-123\n"
"password: secret\n"
'masked_encrypted_extra: \'{"foo": "XXXXXXXXXX"}\'\n'
)
}
exceptions: list[ValidationError] = []
configs = load_configs(
contents=contents,
schemas={"databases/": self._trivial_schema()},
passwords={},
exceptions=exceptions,
ssh_tunnel_passwords={},
ssh_tunnel_private_keys={},
ssh_tunnel_priv_key_passwords={},
encrypted_extra_secrets={file_name: {"$.foo": "actual_secret"}},
)
assert exceptions == []
assert file_name in configs
merged = json.loads(configs[file_name]["masked_encrypted_extra"])
assert merged == {"foo": "actual_secret"}