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]
