feat: support for import/export masked_encrypted_extra (backend) (#38077)

This commit is contained in:
Vitor Avila
2026-03-04 16:26:28 -03:00
committed by GitHub
parent 63e7ee70bf
commit 8c9efe5659
16 changed files with 799 additions and 2 deletions

View File

@@ -1591,6 +1591,14 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
the private_key should be provided in the following format:
`{"databases/MyDatabase.yaml": "my_private_key_password"}`.
type: string
encrypted_extra_secrets:
description: >-
JSON map of sensitive values for masked_encrypted_extra fields.
Keys are file paths (e.g., "databases/db.yaml") and values
are JSON objects mapping JSONPath expressions to secrets.
(e.g., `{"databases/MyDatabase.yaml":
{"$.credentials_info.private_key": "actual_key"}}`).
type: string
responses:
200:
description: Database import result
@@ -1642,6 +1650,11 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
if "ssh_tunnel_private_key_passwords" in request.form
else None
)
encrypted_extra_secrets = (
json.loads(request.form["encrypted_extra_secrets"])
if "encrypted_extra_secrets" in request.form
else None
)
command = ImportDatabasesCommand(
contents,
@@ -1650,6 +1663,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
ssh_tunnel_passwords=ssh_tunnel_passwords,
ssh_tunnel_private_keys=ssh_tunnel_private_keys,
ssh_tunnel_priv_key_passwords=ssh_tunnel_priv_key_passwords,
encrypted_extra_secrets=encrypted_extra_secrets,
)
command.run()
return self.response(200, message="OK")

View File

@@ -59,6 +59,7 @@ from superset.models.core import ConfigurationMethod, Database
from superset.security.analytics_db_safety import check_sqlalchemy_uri
from superset.utils import json
from superset.utils.core import markdown, parse_ssl_cert
from superset.utils.json import get_masked_fields
database_schemas_query_schema = {
"type": "object",
@@ -241,6 +242,15 @@ def encrypted_extra_validator(value: str | None) -> None:
) from ex
def masked_encrypted_extra_validator(value: str) -> None:
"""
Validate that `masked_encrypted_extra` is a valid non-empty JSON string
"""
if value == "{}":
raise ValidationError([_("Field cannot be empty.")])
encrypted_extra_validator(value)
def extra_validator(value: str) -> str:
"""
Validate that extra is a valid JSON string, and that metadata_params
@@ -874,6 +884,9 @@ class ImportV1DatabaseSchema(Schema):
sqlalchemy_uri = fields.String(required=True)
password = fields.String(allow_none=True)
encrypted_extra = fields.String(allow_none=True, validate=encrypted_extra_validator)
masked_encrypted_extra = fields.String(
allow_none=False, validate=masked_encrypted_extra_validator
)
cache_timeout = fields.Integer(allow_none=True)
expose_in_sqllab = fields.Boolean()
allow_run_async = fields.Boolean()
@@ -971,6 +984,55 @@ class ImportV1DatabaseSchema(Schema):
raise ValidationError(exception_messages)
return
@validates_schema
def validate_masked_encrypted_extra(
self, data: dict[str, Any], **kwargs: Any
) -> None:
if "masked_encrypted_extra" not in data:
return
if "encrypted_extra" in data:
raise ValidationError(
"File contains both `encrypted_extra` and `masked_encrypted_extra`"
)
if db.session.query(Database).filter_by(uuid=data["uuid"]).first():
# Existing DB: sensitive values will be revealed from existing
# encrypted_extra in import_database()
return
masked_encrypted_extra = json.loads(data["masked_encrypted_extra"])
# Determine engine spec from sqlalchemy_uri to get sensitive fields
sqlalchemy_uri = data["sqlalchemy_uri"]
url = make_url_safe(sqlalchemy_uri)
backend = url.get_backend_name()
driver = url.get_driver_name()
db_engine_spec = get_engine_spec(backend, driver=driver)
# Check if any sensitive field is still masked
masked_fields = get_masked_fields(
masked_encrypted_extra,
db_engine_spec.encrypted_extra_sensitive_field_paths(),
)
if masked_fields:
encrypted_extra_fields = db_engine_spec.encrypted_extra_sensitive_fields
labels = (
encrypted_extra_fields
if isinstance(encrypted_extra_fields, dict)
else {}
)
raise ValidationError(
{
"_schema": [
f"Must provide value for masked_encrypted_extra field: {field}"
+ (f" ({labels[field]})" if field in labels else "")
for field in masked_fields
]
}
)
def encrypted_field_properties(self, field: Any, **_) -> dict[str, Any]: # type: ignore
ret = {}