You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ma...@apache.org on 2026/10/06 19:45:23 UTC

(superset) 17/23: cover the login-token CSRF exemption with protection on

This is an automated email from the ASF dual-hosted git repository.

madhushreeag pushed a commit to branch madhu-one-time-login-tokens
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 3e4a8363c87afc51a1525e612d2c3f117fb9fac7
Author: madhushree agarwal <ma...@apple.com>
AuthorDate: Tue Oct 6 10:14:34 2026 -0700

    cover the login-token CSRF exemption with protection on
---
 .../security/login_token_api_tests.py              | 101 +++++++++++++++++++++
 1 file changed, 101 insertions(+)

diff --git a/tests/integration_tests/security/login_token_api_tests.py b/tests/integration_tests/security/login_token_api_tests.py
index 78f8e197e04..a7e3ce4f671 100644
--- a/tests/integration_tests/security/login_token_api_tests.py
+++ b/tests/integration_tests/security/login_token_api_tests.py
@@ -23,6 +23,7 @@ and the session cookie that ``login_user`` writes, neither of which the unit
 tests can see.
 """
 
+from contextlib import contextmanager
 from typing import Any
 from unittest.mock import patch
 from uuid import UUID
@@ -31,6 +32,7 @@ from flask_appbuilder.security.sqla.manager import user_updating
 
 from superset import db
 from superset.daos.key_value import KeyValueDAO
+from superset.extensions import csrf
 from superset.key_value.models import KeyValueEntry
 from superset.key_value.types import KeyValueResource
 from superset.utils import json
@@ -67,6 +69,57 @@ def _verbatim_resolver(request: Any, **kwargs: Any) -> dict[str, Any] | None:
     return request.get_json(silent=True) or None
 
 
+@contextmanager
+def csrf_enabled(app: Any) -> Any:
+    """Turn real CSRF protection on for the duration of a test.
+
+    The integration config sets ``WTF_CSRF_ENABLED = False``, so
+    ``SupersetAppInitializer.configure_wtf`` never ran ``csrf.init_app`` and
+    never applied ``WTF_CSRF_EXEMPT_LIST``. Flipping the config alone does
+    nothing: there is no ``before_request`` hook to flip, and the defaults
+    ``validate_csrf`` reads (``WTF_CSRF_METHODS``, ``WTF_CSRF_FIELD_NAME`` and
+    the rest) are only set by ``init_app``.
+
+    So the real ``init_app`` runs, rather than those defaults being restated
+    here -- restating them would mean reimplementing part of what this is meant
+    to verify. It registers a ``before_request`` hook and a context processor,
+    which Flask refuses once a request has been served, so the setup guard is
+    lifted for that call and everything it appended is removed afterwards. This
+    keeps the test independent of its position in the suite.
+    """
+    got_first_request = app._got_first_request  # noqa: SLF001
+    before = list(app.before_request_funcs.get(None, []))
+    processors = list(app.template_context_processors.get(None, []))
+    extension = app.extensions.get("csrf")
+    exempt_views = set(csrf._exempt_views)  # noqa: SLF001
+    config = {
+        key: app.config[key] for key in list(app.config) if key.startswith("WTF_CSRF_")
+    }
+
+    try:
+        app._got_first_request = False  # noqa: SLF001
+        csrf.init_app(app)
+        for view in app.config["WTF_CSRF_EXEMPT_LIST"]:
+            csrf.exempt(view)
+        app._got_first_request = got_first_request  # noqa: SLF001
+        app.config["WTF_CSRF_ENABLED"] = True
+        yield
+    finally:
+        app._got_first_request = got_first_request  # noqa: SLF001
+        app.before_request_funcs[None] = before
+        app.template_context_processors[None] = processors
+        csrf._exempt_views = exempt_views  # noqa: SLF001
+        for key in [k for k in list(app.config) if k.startswith("WTF_CSRF_")]:
+            if key in config:
+                app.config[key] = config[key]
+            else:
+                del app.config[key]
+        if extension is None:
+            app.extensions.pop("csrf", None)
+        else:
+            app.extensions["csrf"] = extension
+
+
 class TestLoginTokenApi(SupersetTestCase):
     def _mint(self, username: str = GAMMA_USERNAME) -> str:
         """Mint a token through the route and return it."""
@@ -308,6 +361,54 @@ class TestLoginTokenApi(SupersetTestCase):
                 )
                 assert response.status_code == 401, response.data
 
+    # --------------------------------------------------------------------- csrf
+
+    @with_feature_flags(LOGIN_TOKEN=True)
+    @with_config({"LOGIN_TOKEN_IDENTITY_RESOLVER": _resolver})
+    def test_mint_is_csrf_exempt_with_protection_actually_on(self):
+        """The mint POST must work with no session and no CSRF token.
+
+        It is a server-to-server call from a backend that holds neither, so the
+        entry in ``WTF_CSRF_EXEMPT_LIST`` is load-bearing: without it the flow
+        cannot be used at all. Nothing else covers it, because the integration
+        config disables CSRF outright -- so removing or misspelling that entry
+        would leave the suite green and break production.
+        """
+        with csrf_enabled(self.app):
+            # Control first. If CSRF were not genuinely active, every assertion
+            # below would pass for the wrong reason, so prove a non-exempt POST
+            # on the same blueprint is rejected before trusting the rest.
+            control = self.client.post(
+                "/api/v1/security/guest_token/",
+                data=json.dumps({}),
+                content_type="application/json",
+            )
+            assert control.status_code == 400, (
+                "CSRF protection is not actually active, so this test proves "
+                f"nothing (guest_token returned {control.status_code})"
+            )
+
+            # The exempt endpoint: no session, no CSRF token, still mints.
+            response = self.client.post(
+                ENDPOINT,
+                data=json.dumps({"username": GAMMA_USERNAME}),
+                content_type="application/json",
+                headers={"X-Test-Mint-Secret": MINT_SECRET},
+            )
+            assert response.status_code == 200, response.data
+            token = json.loads(response.data)["access_token"]
+
+            # A rejected caller must fail on the credential, not on CSRF.
+            rejected = self.client.post(
+                ENDPOINT,
+                data=json.dumps({"username": GAMMA_USERNAME}),
+                content_type="application/json",
+            )
+            assert rejected.status_code == 401, rejected.data
+
+        # The minted token is a real one, redeemable after the fact.
+        assert self.client.get(f"{ENDPOINT}?token={token}").status_code == 302
+
     # --------------------------------------------------------------------- next
 
     @with_feature_flags(LOGIN_TOKEN=True)