mirror of
https://github.com/apache/superset.git
synced 2026-04-24 10:35:01 +00:00
feat: support for import/export masked_encrypted_extra (backend) (#38077)
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user