codeant-ai-for-open-source[bot] commented on code in PR #36856:
URL: https://github.com/apache/superset/pull/36856#discussion_r3573311931


##########
superset/db_engine_specs/snowflake.py:
##########
@@ -44,12 +46,61 @@
 from superset.db_engine_specs.postgres import PostgresBaseEngineSpec
 from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
 from superset.models.sql_lab import Query
+from superset.superset_typing import (
+    OAuth2ClientConfig,
+    OAuth2State,
+)
 from superset.utils import json
 from superset.utils.core import get_user_agent, QuerySource
+from superset.utils.oauth2 import encode_oauth2_state, generate_code_challenge
 
 if TYPE_CHECKING:
     from superset.models.core import Database
 
+try:
+    from snowflake.connector.errors import DatabaseError
+except ImportError:
+    # Use a distinct sentinel type when snowflake is not installed to avoid
+    # matching unrelated exception types (using `Exception` would be too 
broad).
+    class _SnowflakeDatabaseError(Exception):
+        """Sentinel type to stand in for 
snowflake.connector.errors.DatabaseError."""
+
+        pass
+
+    DatabaseError = _SnowflakeDatabaseError
+
+
+class CustomSnowflakeAuthErrorMeta(type):
+    """
+    Metaclass whose ``__instancecheck__`` matches Snowflake's invalid/expired
+    OAuth access-token error, so ``CustomSnowflakeAuthError`` can be used as 
the
+    ``oauth2_exception`` that triggers the OAuth2 re-auth dance.
+
+    This is only honored via ``isinstance()`` (the path used by
+    ``BaseEngineSpec.needs_oauth2()``); ``except`` clauses do not call
+    ``__instancecheck__``, so it must not be relied on for exception catching.
+    """
+
+    def __instancecheck__(cls, instance: object) -> bool:
+        """
+        Match Snowflake's invalid/expired OAuth token error, whether it arrives
+        wrapped by SQLAlchemy (e.g. ``Engine``-based execution) or as the raw
+        DBAPI exception — ``BaseEngineSpec.execute()`` runs against a bare
+        cursor and never wraps it, so both shapes must be handled here.
+        """
+        orig: object = instance
+        if isinstance(instance, SqlalchemyDatabaseError):
+            orig = cast(SqlalchemyDatabaseError, instance).orig
+
+        return isinstance(orig, DatabaseError) and "Invalid OAuth access 
token" in str(
+            orig
+        )

Review Comment:
   **Suggestion:** Storing the OAuth access token in the SQLAlchemy URL makes 
the engine cache key token-dependent (the cache key includes 
`str(sqlalchemy_url)`), so token refreshes create new cached engines 
indefinitely and can cause unbounded engine/pool growth. Keep the URL stable 
and pass the token via connection arguments (or otherwise exclude it from 
engine cache key material) so refreshed tokens do not create new engine cache 
entries. [cache]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ SQL Lab queries create new engines on each token refresh.
   - ❌ Dashboard queries against Snowflake grow engine cache unbounded.
   - ⚠️ Background jobs using Snowflake OAuth may exhaust process memory.
   - ⚠️ Connection pooling becomes ineffective due to fragmented engine cache.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Configure a Snowflake database in Superset using `SnowflakeEngineSpec` 
(class at
   `superset/db_engine_specs/snowflake.py:135`) with `impersonate_user=True` 
and an OAuth2
   client so that `database.is_oauth2_enabled()` returns True for this database.
   
   2. From SQL Lab, run a query against this Snowflake database so that
   `query.database.get_sqla_engine()` is invoked (call site 
`superset/sql_lab.py:23-27`),
   which enters `Database.get_sqla_engine()` at `superset/models/core.py:455`.
   
   3. Inside `Database.get_sqla_engine()`, `_get_sqla_engine()` is called 
(definition around
   `superset/models/core.py:654`), which computes an `access_token` via
   `get_oauth2_access_token(...)` and then, when `self.impersonate_user` is 
True, calls
   `self.db_engine_spec.impersonate_user(...)` (lines 
`superset/models/core.py:22-29` in the
   second snippet), passing `sqlalchemy_url` and `engine_kwargs`.
   
   4. In `SnowflakeEngineSpec.impersonate_user()` (method at
   `superset/db_engine_specs/snowflake.py:58-99`), when `user_token` is present 
and OAuth2 is
   enabled, the URL is mutated to include the token: `url = 
url.update_query_dict({"token":
   user_token})` (line 97), and both the modified `url` and `engine_kwargs` 
(with
   `connect_args["authenticator"] = "oauth"`) are returned.
   
   5. Back in `_get_sqla_engine()`, the per-process engine cache key is 
computed as
   `(self.id, str(sqlalchemy_url), repr(sorted(engine_kwargs.items())))` 
(cache-key
   construction around `superset/models/core.py:616-621`), so the stringified 
URL—including
   its `token` query parameter—is part of the cache key.
   
   6. When the OAuth access token is refreshed (for the same user or for 
different users),
   `SnowflakeEngineSpec.impersonate_user()` again executes 
`url.update_query_dict({"token":
   user_token})` with the new token value, changing `str(sqlalchemy_url)` while 
`self.id` and
   most `engine_kwargs` stay the same.
   
   7. Because `str(sqlalchemy_url)` changes on every token refresh, 
`_ENGINE_CACHE` in
   `superset/models/core.py` treats each token value as a distinct entry, calls
   `create_engine(sqlalchemy_url, **engine_kwargs)` for each new token, and 
stores the new
   engine without ever evicting the old one, leading to unbounded growth of 
cached engines
   for a single Snowflake database under normal interactive query activity.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=c1baa1b396cc447cbf9078f8a4f9e1fa&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=c1baa1b396cc447cbf9078f8a4f9e1fa&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/db_engine_specs/snowflake.py
   **Line:** 97:97
   **Comment:**
        *Cache: Storing the OAuth access token in the SQLAlchemy URL makes the 
engine cache key token-dependent (the cache key includes 
`str(sqlalchemy_url)`), so token refreshes create new cached engines 
indefinitely and can cause unbounded engine/pool growth. Keep the URL stable 
and pass the token via connection arguments (or otherwise exclude it from 
engine cache key material) so refreshed tokens do not create new engine cache 
entries.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36856&comment_hash=a1be5120c4dfc07eda519aed281de566afddcaacda64ee0f56853c2b146c26d9&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36856&comment_hash=a1be5120c4dfc07eda519aed281de566afddcaacda64ee0f56853c2b146c26d9&reaction=dislike'>👎</a>



-- 
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