eschutho opened a new pull request, #43826:
URL: https://github.com/apache/superset/pull/43826

   ### SUMMARY
   
   Fixes 
[SUPERSET-PYTHON-15J6](https://preset-inc.sentry.io/issues/7684373355/) — 
`NoAuthorizationError: Missing JWT in cookies or headers`, **28,783 events** 
since firstSeen 2026-08-21, still firing daily.
   
   **The Sentry-captured exception is not the original problem — it is a 
_secondary_ exception raised while Flask is already handling an unrelated, 
benign exception** (in the sampled events, a 405 `MethodNotAllowed` from a 
request hitting the wrong HTTP method on a route, before any view/auth code 
runs). The same crash can happen for any early-in-request-lifecycle 
`HTTPException`.
   
   #### Root-cause chain
   
   1. Flask raises `MethodNotAllowed` (405) during routing, before any 
view/auth code runs.
   2. The global `@app.errorhandler(HTTPException) def show_http_exception` 
(`superset/views/error_handling.py`) catches it and calls 
`json_error_response(...)`.
   3. `json_error_response` calls `sanitize_superset_errors()` 
(`superset/utils/error_sanitization.py`).
   4. That calls `is_sanitization_required()` → 
`security_manager.is_guest_user()`.
   5. `is_guest_user()` with no `user` argument calls `get_current_user()` 
(`superset/tasks/utils.py`), whose `g.user` access forces resolution of 
flask_login's `current_user` `LocalProxy`.
   6. Resolving the proxy invokes the app's registered request loader. For a 
request with **no guest token, no JWT, and not a public-workspace / MCP-OAuth 
path**, the loader calls `verify_jwt_in_request()` and lets it raise (by design 
— a global flask-jwt-extended handler is meant to turn that into a 401 for a 
real unauthenticated _view_ request).
   7. But here the raise happens **inside `show_http_exception`, the error 
handler for the original 405**. This second exception is not caught by anything 
and propagates out unhandled — captured by Sentry, and turning a benign 405 
into an actual crash/500 for any embedded-enabled deployment, for any 
unauthenticated request that trips an `HTTPException` path before auth ever 
runs.
   
   #### The fix
   
   Wrap the current-user resolution inside 
`SupersetSecurityManager.is_guest_user()` in `try/except NoAuthorizationError: 
return False`.
   
   A request that carries no JWT and no guest token **definitionally cannot be 
an embedded guest viewer**, so returning `False` is the semantically correct 
answer, not merely crash avoidance. `is_guest_user` is the shared choke point 
behind ~14 call sites; only the error-sanitization path is normally reachable 
before auth runs, and this fix protects all of them without changing behavior 
for any already-authenticated caller.
   
   `NoAuthorizationError` is imported from `flask_jwt_extended.exceptions` (the 
top-level package does not re-export it). The fix lives entirely in 
`superset/security/manager.py`; `error_sanitization.py`, `error_handling.py`, 
and the private request-loader are intentionally untouched — their behavior is 
correct for real view requests.
   
   ### TESTING INSTRUCTIONS
   
   Added 
`tests/unit_tests/security/manager_test.py::test_is_guest_user_no_jwt_returns_false_without_raising`:
 with `EMBEDDED_SUPERSET` enabled and the current-user resolution raising 
`NoAuthorizationError`, `is_guest_user()` must return `False` rather than 
propagate.
   
   Verified the test **fails before** the fix (raises `NoAuthorizationError`) 
and **passes after**:
   
   ```
   $ pytest tests/unit_tests/security/manager_test.py 
tests/unit_tests/utils/test_error_sanitization.py -q
   136 passed
   
   $ ruff check superset/security/manager.py 
tests/unit_tests/security/manager_test.py
   All checks passed!
   $ ruff format --check superset/security/manager.py 
tests/unit_tests/security/manager_test.py
   2 files already formatted
   ```
   
   `pre-commit` (mypy, ruff, ruff-format, pylint) clean on the changed files.
   
   ### Tradeoffs
   
   **None as a failure-mode change.** This makes `is_guest_user()` never raise 
where it previously could sometimes crash the request, which is strictly more 
correct:
   
   - For an unauthenticated request with no JWT/guest token, `False` is the 
semantically correct return value (such a request cannot be an embedded guest), 
so no legitimate guest is misclassified.
   - For any already-authenticated caller (real guest token or logged-in user), 
the proxy resolves without raising, so `NoAuthorizationError` is never hit and 
behavior is byte-for-byte unchanged.
   - No security-boundary change: it never grants guest treatment where it 
wasn't already granted — it only stops a benign error path from escalating into 
an unhandled 500. Nothing here is undisclosed.
   
   ### ADDITIONAL INFORMATION
   
   - Sentry: 
[SUPERSET-PYTHON-15J6](https://preset-inc.sentry.io/issues/7684373355/) (28,783 
events, auto-resolves on merge via `Fixes SUPERSET-PYTHON-15J6`)
   - Shortcut: [SC-119741](https://app.shortcut.com/preset/story/119741)
   - [ ] Has associated issue:
   - [ ] Required feature flags:
   - [ ] Changes UI
   - [ ] Includes DB Migration
   - [ ] Introduces new feature or API
   - [ ] Removes existing feature or API
   


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