korbit-ai[bot] commented on code in PR #32231:
URL: https://github.com/apache/superset/pull/32231#discussion_r1952016478


##########
superset-frontend/src/pages/DatabaseList/index.tsx:
##########
@@ -426,6 +431,49 @@ function DatabaseList({
             handleDatabaseEditModal({ database: original, modalOpen: true });
           const handleDelete = () => openDatabaseDeleteModal(original);
           const handleExport = () => handleDatabaseExport(original);
+          const handleResync = () => {
+            shouldResyncPermsInAsyncMode
+              ? addInfoToast(
+                  t('Validating connectivity for %s', original.database_name),
+                )
+              : addInfoToast(
+                  t('Resyncing permissions for %s', original.database_name),
+                );
+            SupersetClient.post({
+              endpoint: `/api/v1/database/${original.id}/resync_permissions/`,
+            })
+              .then(({ response, json }) => {
+                // Sync request
+                if (response.status === 200) {
+                  addSuccessToast(
+                    t(
+                      'Permissions successfully resynced for %s',
+                      original.database_name,
+                    ),
+                  );
+                }
+                // Async request
+                else {
+                  addInfoToast(
+                    t(
+                      'Syncing permissions for %s in the background',
+                      original.database_name,
+                    ),
+                  );
+                }
+              })
+              .catch(
+                createErrorHandler(errMsg =>
+                  addDangerToast(
+                    t(
+                      'An error occurred while resyncing permissions for %s: 
%s',
+                      original.database_name,
+                      errMsg,
+                    ),
+                  ),
+                ),
+              );

Review Comment:
   ### Incomplete Error Handling in Permission Resync <sub>![category Error 
Handling](https://img.shields.io/badge/Error%20Handling-ea580c)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The catch block only displays a toast message without preserving the error 
details or rethrowing the error for global error tracking.
   
   ###### Why this matters
   Loss of valuable debugging information that could help diagnose production 
issues. The error details are only shown to the user but not logged or tracked.
   
   ###### Suggested change ∙ *Feature Preview*
   Modify the catch block to log the error and potentially rethrow it:
   ```typescript
   .catch(error => {
     console.error('Permission resync failed:', error);
     createErrorHandler(errMsg =>
       addDangerToast(
         t(
           'An error occurred while resyncing permissions for %s: %s',
           original.database_name,
           errMsg,
         ),
       ),
     )(error);
     throw error; // If you want to propagate to error monitoring
   });
   ```
   
   
   </details>
   
   <sub>
   
   [![Report a problem with this 
comment](https://img.shields.io/badge/Report%20a%20problem%20with%20this%20comment-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWVjMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS10cmlhbmdsZS1hbGVydCI+PHBhdGggZD0ibTIxLjczIDE4LTgtMTRhMiAyIDAgMCAwLTMuNDggMGwtOCAxNEEyIDIgMCAwIDAgNCAyMWgxNmEyIDIgMCAwIDAgMS43My0zIi8+PHBhdGggZD0iTTEyIDl2NCIvPjxwYXRoIGQ9Ik0xMiAxN2guMDEiLz48L3N2Zz4=)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/3305ef8e-32d3-4972-9c8b-32c8812da93e?suggestedFixEnabled=true)
   
   💬 Chat with Korbit by mentioning @korbit-ai.
   </sub>
   
   <!--- korbi internal id:9bc0ad72-79e8-4b73-b0c8-1610baa4b7f2 -->
   



##########
superset/tasks/permissions.py:
##########
@@ -0,0 +1,56 @@
+# 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 __future__ import annotations
+
+import logging
+
+from flask import current_app, g
+
+from superset import security_manager
+from superset.commands.database.resync_permissions import 
ResyncPermissionsCommand
+from superset.daos.database import DatabaseDAO
+from superset.extensions import celery_app
+
+logger = logging.getLogger(__name__)
+
+
+@celery_app.task(name="resync_database_permissions", soft_time_limit=600)
+def resync_database_permissions(
+    database_id: int, username: str, original_database_name: str
+) -> None:
+    logger.info("Resyncing permissions for DB connection ID %s", database_id)
+    with current_app.test_request_context():
+        try:
+            user = security_manager.get_user_by_username(username)
+            assert user
+            g.user = user

Review Comment:
   ### Unsafe User Impersonation in Celery Task <sub>![category 
Security](https://img.shields.io/badge/Security-e11d48)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The code directly assigns a user to Flask's g.user global context based on a 
username parameter without proper authentication verification in a celery task.
   
   ###### Why this matters
   This allows impersonation of any user within the Celery task context, which 
could bypass authentication controls if the task is invoked with an arbitrary 
username.
   
   ###### Suggested change ∙ *Feature Preview*
   Add authentication verification before allowing user impersonation. For 
example:
   ```python
   user = security_manager.get_user_by_username(username)
   if not user or not security_manager.can_access_database(user, database_id):
       raise SecurityError('Unauthorized access')
   g.user = user
   ```
   
   
   </details>
   
   <sub>
   
   [![Report a problem with this 
comment](https://img.shields.io/badge/Report%20a%20problem%20with%20this%20comment-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWVjMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS10cmlhbmdsZS1hbGVydCI+PHBhdGggZD0ibTIxLjczIDE4LTgtMTRhMiAyIDAgMCAwLTMuNDggMGwtOCAxNEEyIDIgMCAwIDAgNCAyMWgxNmEyIDIgMCAwIDAgMS43My0zIi8+PHBhdGggZD0iTTEyIDl2NCIvPjxwYXRoIGQ9Ik0xMiAxN2guMDEiLz48L3N2Zz4=)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/97528c93-77c7-4f9f-91bf-c9c47254161a?suggestedFixEnabled=true)
   
   💬 Chat with Korbit by mentioning @korbit-ai.
   </sub>
   
   <!--- korbi internal id:608f0e1b-37b5-4394-b5fd-87df36378400 -->
   



##########
superset/commands/database/resync_permissions.py:
##########
@@ -0,0 +1,276 @@
+# 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 __future__ import annotations
+
+import logging
+from functools import partial
+from typing import Iterable
+
+from superset import security_manager
+from superset.commands.base import BaseCommand
+from superset.commands.database.exceptions import (
+    DatabaseConnectionFailedError,
+    DatabaseConnectionResyncPermissionsError,
+    DatabaseNotFoundError,
+)
+from superset.commands.database.utils import ping
+from superset.daos.database import DatabaseDAO
+from superset.daos.dataset import DatasetDAO
+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.decorators import on_error, transaction
+
+logger = logging.getLogger(__name__)
+
+
+class ResyncPermissionsCommand(BaseCommand):
+    """
+    Command to resync database permissions.
+    """
+
+    def __init__(
+        self,
+        model_id: int,
+        old_db_connection_name: str | None = None,
+        db_connection: Database | None = None,
+        ssh_tunnel: SSHTunnel | None = None,
+    ):
+        """
+        Constructor method.
+        """
+        self.db_connection_id = model_id
+        self.old_db_connection_name: str | None = old_db_connection_name
+        self.db_connection: Database | None = db_connection
+        self.db_connection_ssh_tunnel: SSHTunnel | None = ssh_tunnel
+
+    def validate(self) -> None:
+        if not self.db_connection:
+            database = DatabaseDAO.find_by_id(self.db_connection_id)
+            if not database:
+                raise DatabaseNotFoundError()
+            self.db_connection = database
+
+        if not self.old_db_connection_name:
+            self.old_db_connection_name = self.db_connection.database_name
+
+        if not self.db_connection_ssh_tunnel:
+            self.db_connection_ssh_tunnel = DatabaseDAO.get_ssh_tunnel(
+                self.db_connection_id
+            )
+
+        with self.db_connection.get_sqla_engine() as engine:
+            try:
+                alive = ping(engine)
+            except Exception as err:
+                raise DatabaseConnectionFailedError() from err
+
+        if not alive:
+            raise DatabaseConnectionFailedError()
+
+    @transaction(
+        on_error=partial(on_error, 
reraise=DatabaseConnectionResyncPermissionsError)
+    )
+    def run(self) -> None:
+        """
+        Resyncs the permissions for a DB connection.
+        """
+        self.validate()
+
+        # Make mypy happy (these are already checked in validate)
+        assert self.db_connection
+        assert self.old_db_connection_name
+
+        catalogs = (
+            self._get_catalog_names(self.db_connection)
+            if self.db_connection.db_engine_spec.supports_catalog
+            else [None]
+        )
+
+        for catalog in catalogs:
+            try:
+                schemas = self._get_schema_names(self.db_connection, catalog)
+
+                if catalog:
+                    perm = security_manager.get_catalog_perm(
+                        self.old_db_connection_name,
+                        catalog,
+                    )
+                    existing_pvm = security_manager.find_permission_view_menu(
+                        "catalog_access",
+                        perm,
+                    )
+                    if not existing_pvm:
+                        # new catalog
+                        security_manager.add_permission_view_menu(
+                            "catalog_access",
+                            security_manager.get_catalog_perm(
+                                self.db_connection.database_name,
+                                catalog,
+                            ),
+                        )
+                        for schema in schemas:
+                            security_manager.add_permission_view_menu(
+                                "schema_access",
+                                security_manager.get_schema_perm(
+                                    self.db_connection.database_name,
+                                    catalog,
+                                    schema,
+                                ),
+                            )
+                        continue
+            except DatabaseConnectionFailedError:
+                # more than one catalog, move to next
+                if catalog:
+                    logger.warning("Error processing catalog %s", catalog)
+                    continue
+                raise
+
+            # add possible new schemas in catalog
+            self._refresh_schemas(
+                self.old_db_connection_name,
+                self.db_connection.database_name,
+                catalog,
+                schemas,
+            )
+
+            if self.old_db_connection_name != self.db_connection.database_name:
+                self._rename_database_in_permissions(
+                    self.old_db_connection_name,
+                    self.db_connection.database_name,
+                    catalog,
+                    schemas,
+                )
+
+    def _get_catalog_names(self, db_connection: Database) -> set[str]:
+        """
+        Helper method to load catalogs.
+        """
+        try:
+            return db_connection.get_all_catalog_names(
+                force=True,
+                ssh_tunnel=self.db_connection_ssh_tunnel,
+            )
+        except OAuth2RedirectError:
+            # raise OAuth2 exceptions as-is
+            raise
+        except GenericDBException as ex:
+            raise DatabaseConnectionFailedError() from ex
+
+    def _get_schema_names(
+        self, db_connection: Database, catalog: str | None
+    ) -> set[str]:
+        """
+        Helper method to load schemas.
+        """
+        try:
+            return db_connection.get_all_schema_names(
+                force=True,
+                catalog=catalog,
+                ssh_tunnel=self.db_connection_ssh_tunnel,
+            )
+        except OAuth2RedirectError:
+            # raise OAuth2 exceptions as-is
+            raise
+        except GenericDBException as ex:
+            raise DatabaseConnectionFailedError() from ex
+
+    def _refresh_schemas(
+        self,
+        old_db_connection_name: str,
+        new_db_connection_name: str,
+        catalog: str | None,
+        schemas: Iterable[str],
+    ) -> None:
+        """
+        Add new schemas that don't have permissions yet.
+        """
+        for schema in schemas:
+            perm = security_manager.get_schema_perm(
+                old_db_connection_name,
+                catalog,
+                schema,
+            )
+            existing_pvm = security_manager.find_permission_view_menu(
+                "schema_access",
+                perm,
+            )
+            if not existing_pvm:
+                new_name = security_manager.get_schema_perm(
+                    new_db_connection_name,
+                    catalog,
+                    schema,
+                )
+                security_manager.add_permission_view_menu("schema_access", 
new_name)
+
+    def _rename_database_in_permissions(
+        self,
+        old_db_connection_name: str,
+        new_db_connection_name: str,
+        catalog: str | None,
+        schemas: Iterable[str],
+    ) -> None:
+        new_catalog_perm_name = security_manager.get_catalog_perm(
+            new_db_connection_name,
+            catalog,
+        )
+
+        # rename existing catalog permission
+        if catalog:
+            perm = security_manager.get_catalog_perm(
+                old_db_connection_name,
+                catalog,
+            )
+            existing_pvm = security_manager.find_permission_view_menu(
+                "catalog_access",
+                perm,
+            )
+            if existing_pvm:
+                existing_pvm.view_menu.name = new_catalog_perm_name
+
+        for schema in schemas:
+            new_schema_perm_name = security_manager.get_schema_perm(
+                new_db_connection_name,
+                catalog,
+                schema,
+            )
+
+            # rename existing schema permission
+            perm = security_manager.get_schema_perm(
+                old_db_connection_name,
+                catalog,
+                schema,
+            )
+            existing_pvm = security_manager.find_permission_view_menu(
+                "schema_access",
+                perm,
+            )
+            if existing_pvm:
+                existing_pvm.view_menu.name = new_schema_perm_name
+
+            # rename permissions on datasets and charts
+            for dataset in DatabaseDAO.get_datasets(
+                self.db_connection_id,
+                catalog=catalog,
+                schema=schema,
+            ):
+                dataset.catalog_perm = new_catalog_perm_name
+                dataset.schema_perm = new_schema_perm_name
+                for chart in 
DatasetDAO.get_related_objects(dataset.id)["charts"]:
+                    chart.catalog_perm = new_catalog_perm_name
+                    chart.schema_perm = new_schema_perm_name

Review Comment:
   ### N+1 Query Pattern in Permission Updates <sub>![category 
Performance](https://img.shields.io/badge/Performance-4f46e5)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   Nested loops performing database queries for each dataset and its related 
charts, without batching or bulk updates.
   
   ###### Why this matters
   This can lead to N+1 query performance issues when dealing with large 
numbers of datasets and charts, causing excessive database round trips.
   
   ###### Suggested change ∙ *Feature Preview*
   Batch the permission updates using bulk database operations. Consider using 
a bulk update query or collecting all objects first and then updating them in a 
single transaction:
   ```python
   datasets = DatabaseDAO.get_datasets(self.db_connection_id, catalog=catalog, 
schema=schema)
   dataset_ids = [d.id for d in datasets]
   
   # Bulk update datasets
   DatasetDAO.bulk_update_permissions(dataset_ids, 
       catalog_perm=new_catalog_perm_name,
       schema_perm=new_schema_perm_name)
   
   # Bulk update related charts
   DatasetDAO.bulk_update_chart_permissions(dataset_ids,
       catalog_perm=new_catalog_perm_name,
       schema_perm=new_schema_perm_name)
   ```
   
   
   </details>
   
   <sub>
   
   [![Report a problem with this 
comment](https://img.shields.io/badge/Report%20a%20problem%20with%20this%20comment-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWVjMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS10cmlhbmdsZS1hbGVydCI+PHBhdGggZD0ibTIxLjczIDE4LTgtMTRhMiAyIDAgMCAwLTMuNDggMGwtOCAxNEEyIDIgMCAwIDAgNCAyMWgxNmEyIDIgMCAwIDAgMS43My0zIi8+PHBhdGggZD0iTTEyIDl2NCIvPjxwYXRoIGQ9Ik0xMiAxN2guMDEiLz48L3N2Zz4=)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/c9c6bb7a-8447-44a1-806a-33d0b8837be0?suggestedFixEnabled=true)
   
   💬 Chat with Korbit by mentioning @korbit-ai.
   </sub>
   
   <!--- korbi internal id:e9dc1c08-5e22-4957-9f56-629301c40b07 -->
   



##########
superset/commands/database/resync_permissions_async.py:
##########
@@ -0,0 +1,101 @@
+# 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 __future__ import annotations
+
+import logging
+
+from superset import security_manager
+from superset.commands.base import BaseCommand
+from superset.commands.database.exceptions import (
+    DatabaseConnectionFailedError,
+    DatabaseNotFoundError,
+    UserNotFoundError,
+)
+from superset.commands.database.utils import ping
+from superset.daos.database import DatabaseDAO
+from superset.tasks.permissions import resync_database_permissions
+
+logger = logging.getLogger(__name__)
+
+
+class ResyncPermissionsAsyncCommand(BaseCommand):
+    """
+    Command to trigger an async task to resync database permissions.
+    """
+
+    def __init__(
+        self,
+        model_id: int,
+        username: str | None,
+        old_db_connection_name: str | None = None,
+    ):
+        """
+        Constructor method.
+        """
+        self.db_connection_id = model_id
+        self.username = username
+        self.old_db_connection_name = old_db_connection_name
+
+    def validate(self) -> None:
+        """
+        Validates the command before triggering the async task.
+
+        Confirms both the DB connection user exist. Also tests the DB 
connection.
+        """
+        database = DatabaseDAO.find_by_id(self.db_connection_id)
+        if not database:
+            raise DatabaseNotFoundError()
+
+        if not self.old_db_connection_name:
+            self.old_db_connection_name = database.database_name
+
+        if not self.username or not security_manager.get_user_by_username(
+            self.username
+        ):
+            raise UserNotFoundError()
+
+        with database.get_sqla_engine() as engine:
+            # Make sure the connection works before delegating the task
+            try:
+                alive = ping(engine)
+            except Exception as err:
+                logger.error("Could not stablish a DB connection")
+                raise DatabaseConnectionFailedError() from err
+
+        if not alive:
+            logger.error("Could not stablish a DB connection")
+            raise DatabaseConnectionFailedError()

Review Comment:
   ### Misspelled Error Message <sub>![category 
Logging](https://img.shields.io/badge/Logging-4f46e5)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The error message contains a spelling error ('stablish' instead of 
'establish') which could cause confusion in logs
   
   ###### Why this matters
   Incorrect error messages can make troubleshooting more difficult and appear 
unprofessional in production logs
   
   ###### Suggested change ∙ *Feature Preview*
   Correct the spelling in both error messages:
   ```python
   logger.error("Could not establish a DB connection")
   ```
   
   
   </details>
   
   <sub>
   
   [![Report a problem with this 
comment](https://img.shields.io/badge/Report%20a%20problem%20with%20this%20comment-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWVjMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS10cmlhbmdsZS1hbGVydCI+PHBhdGggZD0ibTIxLjczIDE4LTgtMTRhMiAyIDAgMCAwLTMuNDggMGwtOCAxNEEyIDIgMCAwIDAgNCAyMWgxNmEyIDIgMCAwIDAgMS43My0zIi8+PHBhdGggZD0iTTEyIDl2NCIvPjxwYXRoIGQ9Ik0xMiAxN2guMDEiLz48L3N2Zz4=)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/c11cc760-0f80-4ca3-8083-6c414bc3bb30?suggestedFixEnabled=true)
   
   💬 Chat with Korbit by mentioning @korbit-ai.
   </sub>
   
   <!--- korbi internal id:84c5d8be-c8fc-4be5-ae4a-0ec114ae8105 -->
   



##########
superset/config.py:
##########
@@ -1916,6 +1916,15 @@ class ExtraDynamicQueryFilters(TypedDict, total=False):
 CATALOGS_SIMPLIFIED_MIGRATION: bool = False
 
 
+# When updating a DB connection or manually triggering a resync, the command
+# happens in sync mode. If you have a celery worker configured, it's 
recommended
+# to change below config to ``True`` to run this process in async mode. A DB
+# connection might have hundreds of catalogs with thousands of schemas each, 
which
+# considerably increases the time to process it. Running it in async mode 
prevents
+# keeping a web API call open for this long.
+RESYNC_DB_PERMISSIONS_IN_ASYNC_MODE: bool = False

Review Comment:
   ### Default Async Mode Contradicts Intent <sub>![category 
Functionality](https://img.shields.io/badge/Functionality-0284c7)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The default value for RESYNC_DB_PERMISSIONS_IN_ASYNC_MODE is set to False, 
which contradicts the developer's intent of enabling more efficient 
asynchronous resynchronization of database permissions.
   
   ###### Why this matters
   With the default set to False, all users will experience synchronous 
permission resynchronization by default, potentially causing web API calls to 
hang for long periods when dealing with large datasets.
   
   ###### Suggested change ∙ *Feature Preview*
   Change the default value to True to align with the stated performance goals:
   ```python
   RESYNC_DB_PERMISSIONS_IN_ASYNC_MODE: bool = True
   ```
   
   
   </details>
   
   <sub>
   
   [![Report a problem with this 
comment](https://img.shields.io/badge/Report%20a%20problem%20with%20this%20comment-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWVjMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS10cmlhbmdsZS1hbGVydCI+PHBhdGggZD0ibTIxLjczIDE4LTgtMTRhMiAyIDAgMCAwLTMuNDggMGwtOCAxNEEyIDIgMCAwIDAgNCAyMWgxNmEyIDIgMCAwIDAgMS43My0zIi8+PHBhdGggZD0iTTEyIDl2NCIvPjxwYXRoIGQ9Ik0xMiAxN2guMDEiLz48L3N2Zz4=)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/dc4ea233-468f-45b7-993a-fab8911d654a?suggestedFixEnabled=true)
   
   💬 Chat with Korbit by mentioning @korbit-ai.
   </sub>
   
   <!--- korbi internal id:9212eb3a-692d-43fb-958b-1a006ca57928 -->
   



##########
superset/commands/database/utils.py:
##########
@@ -17,15 +17,33 @@
 from __future__ import annotations
 
 import logging
+import sqlite3
+from contextlib import closing
+
+from flask import current_app as app
+from sqlalchemy.engine import Engine
 
 from superset import security_manager
 from superset.databases.ssh_tunnel.models import SSHTunnel
 from superset.db_engine_specs.base import GenericDBException
 from superset.models.core import Database
+from superset.utils.core import timeout
 
 logger = logging.getLogger(__name__)
 
 
+def ping(engine: Engine) -> bool:
+    try:
+        time_delta = app.config["TEST_DATABASE_CONNECTION_TIMEOUT"]
+        with timeout(int(time_delta.total_seconds())):
+            with closing(engine.raw_connection()) as conn:
+                return engine.dialect.do_ping(conn)
+    except (sqlite3.ProgrammingError, RuntimeError):
+        # SQLite can't run on a separate thread, so ``utils.timeout`` fails
+        # RuntimeError catches the equivalent error from duckdb.
+        return engine.dialect.do_ping(engine)

Review Comment:
   ### Silent Error Handling in Database Ping <sub>![category Error 
Handling](https://img.shields.io/badge/Error%20Handling-ea580c)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The error handling in the ping function silently continues execution without 
logging the error details.
   
   ###### Why this matters
   Without logging the error, it becomes difficult to diagnose issues when 
these specific database errors occur in production.
   
   ###### Suggested change ∙ *Feature Preview*
   Add error logging before continuing execution:
   ```python
   except (sqlite3.ProgrammingError, RuntimeError) as e:
       logger.warning("Database ping timeout mechanism failed, falling back to 
direct ping: %s", str(e))
       return engine.dialect.do_ping(engine)
   ```
   
   
   </details>
   
   <sub>
   
   [![Report a problem with this 
comment](https://img.shields.io/badge/Report%20a%20problem%20with%20this%20comment-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWVjMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS10cmlhbmdsZS1hbGVydCI+PHBhdGggZD0ibTIxLjczIDE4LTgtMTRhMiAyIDAgMCAwLTMuNDggMGwtOCAxNEEyIDIgMCAwIDAgNCAyMWgxNmEyIDIgMCAwIDAgMS43My0zIi8+PHBhdGggZD0iTTEyIDl2NCIvPjxwYXRoIGQ9Ik0xMiAxN2guMDEiLz48L3N2Zz4=)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/67b12db9-3c68-4777-98e0-0ed533be7e3d?suggestedFixEnabled=true)
   
   💬 Chat with Korbit by mentioning @korbit-ai.
   </sub>
   
   <!--- korbi internal id:7e7253a4-5239-4963-aa2f-891a35fe4754 -->
   



##########
superset/tasks/permissions.py:
##########
@@ -0,0 +1,56 @@
+# 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 __future__ import annotations
+
+import logging
+
+from flask import current_app, g
+
+from superset import security_manager
+from superset.commands.database.resync_permissions import 
ResyncPermissionsCommand
+from superset.daos.database import DatabaseDAO
+from superset.extensions import celery_app
+
+logger = logging.getLogger(__name__)
+
+
+@celery_app.task(name="resync_database_permissions", soft_time_limit=600)
+def resync_database_permissions(
+    database_id: int, username: str, original_database_name: str
+) -> None:
+    logger.info("Resyncing permissions for DB connection ID %s", database_id)
+    with current_app.test_request_context():
+        try:
+            user = security_manager.get_user_by_username(username)
+            assert user

Review Comment:
   ### Unsafe User Existence Check <sub>![category Error 
Handling](https://img.shields.io/badge/Error%20Handling-ea580c)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The code uses an assert statement to check for user existence, which could 
be stripped in production when running Python with -O or -OO flags.
   
   ###### Why this matters
   If Python optimization is enabled in production, this check will be removed, 
potentially leading to NoneType errors when trying to access user attributes.
   
   ###### Suggested change ∙ *Feature Preview*
   Replace the assert with an explicit check and raise a proper exception:
   ```python
   if not user:
       raise ValueError(f"User {username} not found")
   ```
   
   
   </details>
   
   <sub>
   
   [![Report a problem with this 
comment](https://img.shields.io/badge/Report%20a%20problem%20with%20this%20comment-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWVjMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS10cmlhbmdsZS1hbGVydCI+PHBhdGggZD0ibTIxLjczIDE4LTgtMTRhMiAyIDAgMCAwLTMuNDggMGwtOCAxNEEyIDIgMCAwIDAgNCAyMWgxNmEyIDIgMCAwIDAgMS43My0zIi8+PHBhdGggZD0iTTEyIDl2NCIvPjxwYXRoIGQ9Ik0xMiAxN2guMDEiLz48L3N2Zz4=)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/48001443-fe11-4bef-9dc5-04701ee28f8c?suggestedFixEnabled=true)
   
   💬 Chat with Korbit by mentioning @korbit-ai.
   </sub>
   
   <!--- korbi internal id:8f78ca04-a9e1-4073-8477-90912dbf9eea -->
   



##########
superset/commands/database/resync_permissions.py:
##########
@@ -0,0 +1,276 @@
+# 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 __future__ import annotations
+
+import logging
+from functools import partial
+from typing import Iterable
+
+from superset import security_manager
+from superset.commands.base import BaseCommand
+from superset.commands.database.exceptions import (
+    DatabaseConnectionFailedError,
+    DatabaseConnectionResyncPermissionsError,
+    DatabaseNotFoundError,
+)
+from superset.commands.database.utils import ping
+from superset.daos.database import DatabaseDAO
+from superset.daos.dataset import DatasetDAO
+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.decorators import on_error, transaction
+
+logger = logging.getLogger(__name__)
+
+
+class ResyncPermissionsCommand(BaseCommand):
+    """
+    Command to resync database permissions.
+    """
+
+    def __init__(
+        self,
+        model_id: int,
+        old_db_connection_name: str | None = None,
+        db_connection: Database | None = None,
+        ssh_tunnel: SSHTunnel | None = None,
+    ):
+        """
+        Constructor method.
+        """
+        self.db_connection_id = model_id
+        self.old_db_connection_name: str | None = old_db_connection_name
+        self.db_connection: Database | None = db_connection
+        self.db_connection_ssh_tunnel: SSHTunnel | None = ssh_tunnel
+
+    def validate(self) -> None:
+        if not self.db_connection:
+            database = DatabaseDAO.find_by_id(self.db_connection_id)
+            if not database:
+                raise DatabaseNotFoundError()
+            self.db_connection = database
+
+        if not self.old_db_connection_name:
+            self.old_db_connection_name = self.db_connection.database_name
+
+        if not self.db_connection_ssh_tunnel:
+            self.db_connection_ssh_tunnel = DatabaseDAO.get_ssh_tunnel(
+                self.db_connection_id
+            )
+
+        with self.db_connection.get_sqla_engine() as engine:
+            try:
+                alive = ping(engine)
+            except Exception as err:
+                raise DatabaseConnectionFailedError() from err
+
+        if not alive:
+            raise DatabaseConnectionFailedError()
+
+    @transaction(
+        on_error=partial(on_error, 
reraise=DatabaseConnectionResyncPermissionsError)
+    )
+    def run(self) -> None:
+        """
+        Resyncs the permissions for a DB connection.
+        """
+        self.validate()
+
+        # Make mypy happy (these are already checked in validate)
+        assert self.db_connection
+        assert self.old_db_connection_name
+
+        catalogs = (
+            self._get_catalog_names(self.db_connection)
+            if self.db_connection.db_engine_spec.supports_catalog
+            else [None]
+        )
+
+        for catalog in catalogs:
+            try:
+                schemas = self._get_schema_names(self.db_connection, catalog)
+
+                if catalog:
+                    perm = security_manager.get_catalog_perm(
+                        self.old_db_connection_name,
+                        catalog,
+                    )
+                    existing_pvm = security_manager.find_permission_view_menu(
+                        "catalog_access",
+                        perm,
+                    )
+                    if not existing_pvm:
+                        # new catalog
+                        security_manager.add_permission_view_menu(
+                            "catalog_access",
+                            security_manager.get_catalog_perm(
+                                self.db_connection.database_name,
+                                catalog,
+                            ),
+                        )
+                        for schema in schemas:
+                            security_manager.add_permission_view_menu(
+                                "schema_access",
+                                security_manager.get_schema_perm(
+                                    self.db_connection.database_name,
+                                    catalog,
+                                    schema,
+                                ),
+                            )
+                        continue
+            except DatabaseConnectionFailedError:
+                # more than one catalog, move to next
+                if catalog:
+                    logger.warning("Error processing catalog %s", catalog)
+                    continue
+                raise

Review Comment:
   ### Inconsistent Catalog Connection Error Handling <sub>![category Error 
Handling](https://img.shields.io/badge/Error%20Handling-ea580c)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The error handling logic for catalog processing silently continues on 
connection failures when catalog is not None, but raises the error when catalog 
is None
   
   ###### Why this matters
   This inconsistent error handling could mask serious connection issues for 
catalogs and lead to incomplete permission synchronization without proper 
notification
   
   ###### Suggested change ∙ *Feature Preview*
   Implement consistent error handling for all catalog cases, preferably 
logging the error and collecting all failures for a summary report:
   ```python
   try:
       schemas = self._get_schema_names(self.db_connection, catalog)
   except DatabaseConnectionFailedError as e:
       logger.error(f"Error processing catalog {catalog}: {str(e)}")
       self.errors.append((catalog, str(e)))
       if not catalog:  # If this is the default catalog, we should fail
           raise
       continue
   ```
   
   
   </details>
   
   <sub>
   
   [![Report a problem with this 
comment](https://img.shields.io/badge/Report%20a%20problem%20with%20this%20comment-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWVjMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS10cmlhbmdsZS1hbGVydCI+PHBhdGggZD0ibTIxLjczIDE4LTgtMTRhMiAyIDAgMCAwLTMuNDggMGwtOCAxNEEyIDIgMCAwIDAgNCAyMWgxNmEyIDIgMCAwIDAgMS43My0zIi8+PHBhdGggZD0iTTEyIDl2NCIvPjxwYXRoIGQ9Ik0xMiAxN2guMDEiLz48L3N2Zz4=)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/31d9a5e8-aeca-482b-a5d0-59974f3fa479?suggestedFixEnabled=true)
   
   💬 Chat with Korbit by mentioning @korbit-ai.
   </sub>
   
   <!--- korbi internal id:a747fef8-68b7-481f-b8d1-1e2360958185 -->
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to