# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. from unittest.mock import MagicMock import pytest from pytest_mock import MockerFixture from superset.commands.database.update import UpdateDatabaseCommand from superset.exceptions import OAuth2RedirectError from superset.extensions import security_manager @pytest.fixture() def database_with_catalog(mocker: MockerFixture) -> MagicMock: """ Mock a database with catalogs and schemas. """ database = mocker.MagicMock() database.database_name = "my_db" database.db_engine_spec.__name__ = "test_engine" database.db_engine_spec.supports_catalog = True database.get_all_catalog_names.return_value = ["catalog1", "catalog2"] database.get_all_schema_names.side_effect = [ ["schema1", "schema2"], ["schema3", "schema4"], ] database.get_default_catalog.return_value = "catalog2" return database @pytest.fixture() def database_without_catalog(mocker: MockerFixture) -> MagicMock: """ Mock a database without catalogs. """ database = mocker.MagicMock() database.database_name = "my_db" database.db_engine_spec.__name__ = "test_engine" database.db_engine_spec.supports_catalog = False database.get_all_schema_names.return_value = ["schema1", "schema2"] return database @pytest.fixture() def database_needs_oauth2(mocker: MockerFixture) -> MagicMock: """ Mock a database without catalogs that needs OAuth2. """ database = mocker.MagicMock() database.database_name = "my_db" database.db_engine_spec.__name__ = "test_engine" database.db_engine_spec.supports_catalog = False database.get_all_schema_names.side_effect = OAuth2RedirectError( "url", "tab_id", "redirect_uri", ) return database def test_update_with_catalog( mocker: MockerFixture, database_with_catalog: MockerFixture, ) -> None: """ Test that permissions are updated correctly. In this test, the database has two catalogs with two schemas each: - catalog1 - schema1 - schema2 - catalog2 - schema3 - schema4 When update is called, only `catalog2.schema3` has permissions associated with it, so `catalog1.*` and `catalog2.schema4` are added. """ DatabaseDAO = mocker.patch("superset.commands.database.update.DatabaseDAO") DatabaseDAO.find_by_id.return_value = database_with_catalog DatabaseDAO.update.return_value = database_with_catalog find_permission_view_menu = mocker.patch.object( security_manager, "find_permission_view_menu", ) find_permission_view_menu.side_effect = [ None, # first catalog is new "[my_db].[catalog2]", # second catalog already exists "[my_db].[catalog2].[schema3]", # first schema already exists None, # second schema is new # these are called when checking for existing perms in [db].[schema] format None, None, ] add_permission_view_menu = mocker.patch.object( security_manager, "add_permission_view_menu", ) UpdateDatabaseCommand(1, {}).run() add_permission_view_menu.assert_has_calls( [ # first catalog is added with all schemas mocker.call("catalog_access", "[my_db].[catalog1]"), mocker.call("schema_access", "[my_db].[catalog1].[schema1]"), mocker.call("schema_access", "[my_db].[catalog1].[schema2]"), # second catalog already exists, only `schema4` is added mocker.call("schema_access", "[my_db].[catalog2].[schema4]"), ], ) def test_update_without_catalog( mocker: MockerFixture, database_without_catalog: MockerFixture, ) -> None: """ Test that permissions are updated correctly. In this test, the database has no catalogs and two schemas: - schema1 - schema2 When update is called, only `schema2` has permissions associated with it, so `schema1` is added. """ DatabaseDAO = mocker.patch("superset.commands.database.update.DatabaseDAO") DatabaseDAO.find_by_id.return_value = database_without_catalog DatabaseDAO.update.return_value = database_without_catalog find_permission_view_menu = mocker.patch.object( security_manager, "find_permission_view_menu", ) find_permission_view_menu.side_effect = [ None, # schema1 has no permissions "[my_db].[schema2]", # second schema already exists ] add_permission_view_menu = mocker.patch.object( security_manager, "add_permission_view_menu", ) UpdateDatabaseCommand(1, {}).run() add_permission_view_menu.assert_called_with( "schema_access", "[my_db].[schema1]", ) def test_rename_with_catalog( mocker: MockerFixture, database_with_catalog: MockerFixture, ) -> None: """ Test that permissions are renamed correctly. In this test, the database has two catalogs with two schemas each: - catalog1 - schema1 - schema2 - catalog2 - schema3 - schema4 When update is called, only `catalog2.schema3` has permissions associated with it, so `catalog1.*` and `catalog2.schema4` are added. Additionally, the database has been renamed from `my_db` to `my_other_db`. """ DatabaseDAO = mocker.patch("superset.commands.database.update.DatabaseDAO") original_database = mocker.MagicMock() original_database.database_name = "my_db" DatabaseDAO.find_by_id.return_value = original_database database_with_catalog.database_name = "my_other_db" DatabaseDAO.update.return_value = database_with_catalog dataset = mocker.MagicMock() chart = mocker.MagicMock() DatabaseDAO.get_datasets.return_value = [dataset] DatasetDAO = mocker.patch("superset.commands.database.update.DatasetDAO") DatasetDAO.get_related_objects.return_value = {"charts": [chart]} find_permission_view_menu = mocker.patch.object( security_manager, "find_permission_view_menu", ) catalog2_pvm = mocker.MagicMock() catalog2_schema3_pvm = mocker.MagicMock() find_permission_view_menu.side_effect = [ # these are called when adding the permissions: None, # first catalog is new "[my_db].[catalog2]", # second catalog already exists "[my_db].[catalog2].[schema3]", # first schema already exists None, # second schema is new # these are called when renaming the permissions: catalog2_pvm, # old [my_db].[catalog2] catalog2_schema3_pvm, # old [my_db].[catalog2].[schema3] None, # [my_db].[catalog2].[schema4] doesn't exist ] add_permission_view_menu = mocker.patch.object( security_manager, "add_permission_view_menu", ) UpdateDatabaseCommand(1, {}).run() add_permission_view_menu.assert_has_calls( [ # first catalog is added with all schemas with the new DB name mocker.call("catalog_access", "[my_other_db].[catalog1]"), mocker.call("schema_access", "[my_other_db].[catalog1].[schema1]"), mocker.call("schema_access", "[my_other_db].[catalog1].[schema2]"), # second catalog already exists, only `schema4` is added mocker.call("schema_access", "[my_other_db].[catalog2].[schema4]"), ], ) assert catalog2_pvm.view_menu.name == "[my_other_db].[catalog2]" assert catalog2_schema3_pvm.view_menu.name == "[my_other_db].[catalog2].[schema3]" assert dataset.catalog_perm == "[my_other_db].[catalog2]" assert dataset.schema_perm == "[my_other_db].[catalog2].[schema4]" assert chart.catalog_perm == "[my_other_db].[catalog2]" assert chart.schema_perm == "[my_other_db].[catalog2].[schema4]" def test_rename_without_catalog( mocker: MockerFixture, database_without_catalog: MockerFixture, ) -> None: """ Test that permissions are renamed correctly. In this test, the database has no catalogs and two schemas: - schema1 - schema2 When update is called, only `schema2` has permissions associated with it, so `schema1` is added. Additionally, the database has been renamed from `my_db` to `my_other_db`. """ DatabaseDAO = mocker.patch("superset.commands.database.update.DatabaseDAO") original_database = mocker.MagicMock() original_database.database_name = "my_db" DatabaseDAO.find_by_id.return_value = original_database database_without_catalog.database_name = "my_other_db" DatabaseDAO.update.return_value = database_without_catalog DatabaseDAO.get_datasets.return_value = [] find_permission_view_menu = mocker.patch.object( security_manager, "find_permission_view_menu", ) schema2_pvm = mocker.MagicMock() find_permission_view_menu.side_effect = [ None, # schema1 has no permissions "[my_db].[schema2]", # second schema already exists None, # [my_db].[schema1] doesn't exist schema2_pvm, # old [my_db].[schema2] ] add_permission_view_menu = mocker.patch.object( security_manager, "add_permission_view_menu", ) UpdateDatabaseCommand(1, {}).run() add_permission_view_menu.assert_called_with( "schema_access", "[my_other_db].[schema1]", ) assert schema2_pvm.view_menu.name == "[my_other_db].[schema2]" def test_update_with_oauth2( mocker: MockerFixture, database_needs_oauth2: MockerFixture, ) -> None: """ Test that the database can be updated even if OAuth2 is needed to connect. """ DatabaseDAO = mocker.patch("superset.commands.database.update.DatabaseDAO") 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, # schema1 has no permissions "[my_db].[schema2]", # second schema already exists ] add_permission_view_menu = mocker.patch.object( security_manager, "add_permission_view_menu", ) UpdateDatabaseCommand(1, {}).run() add_permission_view_menu.assert_not_called()