From a6ad0bf1692574405f80903bcc51fd1ff7804a6e Mon Sep 17 00:00:00 2001 From: Andy <41118244+andy-clapson@users.noreply.github.com> Date: Tue, 12 May 2026 00:16:41 -0400 Subject: [PATCH] =?UTF-8?q?fix(re-encrypt-secrets):=20use=20db.Model.metad?= =?UTF-8?q?ata=20to=20discover=20encrypted=20=E2=80=A6=20(#39390)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Opus 4.7 (1M context) --- superset/utils/encrypt.py | 21 ++++++++++++++++--- .../integration_tests/utils/encrypt_tests.py | 15 +++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/superset/utils/encrypt.py b/superset/utils/encrypt.py index b833fc79de8..bf1c5b159c4 100644 --- a/superset/utils/encrypt.py +++ b/superset/utils/encrypt.py @@ -99,14 +99,29 @@ class SecretsMigrator: def discover_encrypted_fields(self) -> dict[str, dict[str, EncryptedType]]: """ - Iterates over SqlAlchemy's metadata, looking for EncryptedType - columns along the way. Builds up a dict of + Iterates over ORM-mapped tables, looking for EncryptedType columns + along the way. Builds up a dict of table_name -> dict of col_name: enc type instance - :return: + + Superset's ORM models inherit from Flask-AppBuilder's declarative base + (`flask_appbuilder.Model`), whose MetaData is distinct from + `db.metadata`. We combine both sources so encrypted columns are found + regardless of which base a model uses. FAB's metadata takes precedence + when a table name appears in both registries. + + :return: mapping of table name to a dict of {column name: EncryptedType} """ + from flask_appbuilder import ( # pylint: disable=import-outside-toplevel + Model as FABModel, + ) + meta_info: dict[str, Any] = {} + tables: dict[str, Any] = dict(FABModel.metadata.tables) for table_name, table in self._db.metadata.tables.items(): + tables.setdefault(table_name, table) + + for table_name, table in tables.items(): for col_name, col in table.columns.items(): if isinstance(col.type, EncryptedType): cols = meta_info.get(table_name, {}) diff --git a/tests/integration_tests/utils/encrypt_tests.py b/tests/integration_tests/utils/encrypt_tests.py index 95279ee607e..e791411ca4e 100644 --- a/tests/integration_tests/utils/encrypt_tests.py +++ b/tests/integration_tests/utils/encrypt_tests.py @@ -89,6 +89,21 @@ class EncryptedFieldTest(SupersetTestCase): " encrypted_field_factory" ) + def test_discover_encrypted_fields_finds_dbs_table(self): + """ + Ensure discover_encrypted_fields finds the encrypted columns on the + dbs table (password, encrypted_extra, server_cert). This guards + against db.metadata diverging from db.Model.metadata. + """ + migrator = SecretsMigrator("") + encrypted_fields = migrator.discover_encrypted_fields() + assert "dbs" in encrypted_fields, ( + "dbs table not found in encrypted fields — " + "discover_encrypted_fields may be using the wrong MetaData instance" + ) + dbs_cols = set(encrypted_fields["dbs"].keys()) + assert {"password", "encrypted_extra", "server_cert"}.issubset(dbs_cols) + def test_lazy_key_resolution(self): """ Verify that the encryption key is resolved lazily at runtime,