This is an automated email from the ASF dual-hosted git repository.

vavila pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 41ed37ab02 fix(oauth): Handle updates to the OAuth config (#31777)
41ed37ab02 is described below

commit 41ed37ab02b79ef00b99c87d5678d52eeab0bed6
Author: Vitor Avila <[email protected]>
AuthorDate: Fri Jan 10 15:54:53 2025 -0300

    fix(oauth): Handle updates to the OAuth config (#31777)
---
 superset/commands/database/update.py               | 29 ++++---
 tests/unit_tests/commands/databases/update_test.py | 89 +++++++++++++++++++---
 2 files changed, 98 insertions(+), 20 deletions(-)

diff --git a/superset/commands/database/update.py 
b/superset/commands/database/update.py
index 85439afa71..8cc5e42451 100644
--- a/superset/commands/database/update.py
+++ b/superset/commands/database/update.py
@@ -44,6 +44,7 @@ from superset.databases.ssh_tunnel.models import SSHTunnel
 from superset.db_engine_specs.base import GenericDBException
 from superset.exceptions import OAuth2RedirectError
 from superset.models.core import Database
+from superset.utils import json
 from superset.utils.decorators import on_error, transaction
 
 logger = logging.getLogger(__name__)
@@ -66,22 +67,23 @@ class UpdateDatabaseCommand(BaseCommand):
 
         self.validate()
 
-        # unmask ``encrypted_extra``
-        self._properties["encrypted_extra"] = (
-            self._model.db_engine_spec.unmask_encrypted_extra(
-                self._model.encrypted_extra,
-                self._properties.pop("masked_encrypted_extra", "{}"),
+        if "masked_encrypted_extra" in self._properties:
+            # unmask ``encrypted_extra``
+            self._properties["encrypted_extra"] = (
+                self._model.db_engine_spec.unmask_encrypted_extra(
+                    self._model.encrypted_extra,
+                    self._properties["masked_encrypted_extra"],
+                )
             )
-        )
+
+            # Depending on the changes to the OAuth2 configuration we may need 
to purge
+            # existing personal tokens.
+            self._handle_oauth2()
 
         # if the database name changed we need to update any existing 
permissions,
         # since they're name based
         original_database_name = self._model.database_name
 
-        # Depending on the changes to the OAuth2 configuration we may need to 
purge
-        # existing personal tokens.
-        self._handle_oauth2()
-
         database = DatabaseDAO.update(self._model, self._properties)
         database.set_sqlalchemy_uri(database.sqlalchemy_uri)
         ssh_tunnel = self._handle_ssh_tunnel(database)
@@ -99,11 +101,16 @@ class UpdateDatabaseCommand(BaseCommand):
         if not self._model:
             return
 
+        if self._properties["encrypted_extra"] is None:
+            self._model.purge_oauth2_tokens()
+            return
+
         current_config = self._model.get_oauth2_config()
         if not current_config:
             return
 
-        new_config = 
self._properties["encrypted_extra"].get("oauth2_client_info", {})
+        encrypted_extra = json.loads(self._properties["encrypted_extra"])
+        new_config = encrypted_extra.get("oauth2_client_info", {})
 
         # Keys that require purging personal tokens because they probably are 
no longer
         # valid. For example, if the scope has changed the existing tokens are 
still
diff --git a/tests/unit_tests/commands/databases/update_test.py 
b/tests/unit_tests/commands/databases/update_test.py
index 7ca3d70dc4..a7a1dd97c5 100644
--- a/tests/unit_tests/commands/databases/update_test.py
+++ b/tests/unit_tests/commands/databases/update_test.py
@@ -21,8 +21,10 @@ import pytest
 from pytest_mock import MockerFixture
 
 from superset.commands.database.update import UpdateDatabaseCommand
+from superset.db_engine_specs.base import BaseEngineSpec
 from superset.exceptions import OAuth2RedirectError
 from superset.extensions import security_manager
+from superset.utils import json
 
 oauth2_client_info = {
     "id": "client_id",
@@ -82,7 +84,10 @@ def database_needs_oauth2(mocker: MockerFixture) -> 
MagicMock:
         "tab_id",
         "redirect_uri",
     )
-    database.get_oauth2_config.return_value = oauth2_client_info
+    database.encrypted_extra = json.dumps({"oauth2_client_info": 
oauth2_client_info})
+    database.db_engine_spec.unmask_encrypted_extra = (
+        BaseEngineSpec.unmask_encrypted_extra
+    )
 
     return database
 
@@ -332,10 +337,6 @@ def test_update_with_oauth2(
         "add_permission_view_menu",
     )
 
-    database_needs_oauth2.db_engine_spec.unmask_encrypted_extra.return_value = 
{
-        "oauth2_client_info": oauth2_client_info,
-    }
-
     UpdateDatabaseCommand(1, {}).run()
 
     add_permission_view_menu.assert_not_called()
@@ -368,11 +369,81 @@ def test_update_with_oauth2_changed(
 
     modified_oauth2_client_info = oauth2_client_info.copy()
     modified_oauth2_client_info["scope"] = "scope-b"
-    database_needs_oauth2.db_engine_spec.unmask_encrypted_extra.return_value = 
{
-        "oauth2_client_info": modified_oauth2_client_info,
-    }
 
-    UpdateDatabaseCommand(1, {}).run()
+    UpdateDatabaseCommand(
+        1,
+        {
+            "masked_encrypted_extra": json.dumps(
+                {"oauth2_client_info": modified_oauth2_client_info}
+            )
+        },
+    ).run()
+
+    add_permission_view_menu.assert_not_called()
+    database_needs_oauth2.purge_oauth2_tokens.assert_called()
+
+
+def test_remove_oauth_config_purges_tokens(
+    mocker: MockerFixture,
+    database_needs_oauth2: MockerFixture,
+) -> None:
+    """
+    Test that removing the OAuth config from a database purges existing tokens.
+    """
+    DatabaseDAO = 
mocker.patch("superset.commands.database.update.DatabaseDAO")  # noqa: N806
+    DatabaseDAO.find_by_id.return_value = database_needs_oauth2
+    DatabaseDAO.update.return_value = database_needs_oauth2
+
+    find_permission_view_menu = mocker.patch.object(
+        security_manager,
+        "find_permission_view_menu",
+    )
+    find_permission_view_menu.side_effect = [
+        None,
+        "[my_db].[schema2]",
+    ]
+    add_permission_view_menu = mocker.patch.object(
+        security_manager,
+        "add_permission_view_menu",
+    )
+
+    UpdateDatabaseCommand(1, {"masked_encrypted_extra": None}).run()
 
     add_permission_view_menu.assert_not_called()
     database_needs_oauth2.purge_oauth2_tokens.assert_called()
+
+    UpdateDatabaseCommand(1, {"masked_encrypted_extra": "{}"}).run()
+
+    add_permission_view_menu.assert_not_called()
+    database_needs_oauth2.purge_oauth2_tokens.assert_called()
+
+
+def test_update_other_fields_dont_affect_oauth(
+    mocker: MockerFixture,
+    database_needs_oauth2: MockerFixture,
+) -> None:
+    """
+    Test that not including ``masked_encrypted_extra`` in the payload does not
+    touch the OAuth config.
+    """
+    DatabaseDAO = 
mocker.patch("superset.commands.database.update.DatabaseDAO")  # noqa: N806
+    DatabaseDAO.find_by_id.return_value = database_needs_oauth2
+    DatabaseDAO.update.return_value = database_needs_oauth2
+
+    find_permission_view_menu = mocker.patch.object(
+        security_manager,
+        "find_permission_view_menu",
+    )
+    find_permission_view_menu.side_effect = [
+        None,
+        "[my_db].[schema2]",
+    ]
+    add_permission_view_menu = mocker.patch.object(
+        security_manager,
+        "add_permission_view_menu",
+    )
+
+    UpdateDatabaseCommand(1, {"database_name": "New DB name"}).run()
+
+    add_permission_view_menu.assert_not_called()
+    database_needs_oauth2.purge_oauth2_tokens.assert_not_called()

Reply via email to