aminghadersohi commented on code in PR #45639:
URL: https://github.com/apache/superset/pull/45639#discussion_r4364785125


##########
superset/mcp_service/auth.py:
##########
@@ -595,6 +596,40 @@ def load_user_with_relationships(
     return security_manager.find_user_with_relationships(username=username, 
email=email)
 
 
+def _reverify_guest_bearer(access_token: Any) -> dict[str, Any]:
+    """Re-verify a guest-marked token's raw bearer as a Superset guest token.
+
+    The guest marker and ``client_id == "guest"`` identify a token admitted by
+    ``GuestTokenVerifier``, but neither is bound to the bearer: a token source
+    that does not strip the marker (e.g. a verifier wrapped next to the
+    ``CompositeTokenVerifier`` in a FastMCP ``MultiAuth``) can pass both 
through
+    from an external IdP JWT. Decoding the bearer with 
``GUEST_TOKEN_JWT_SECRET``
+    binds the guest identity to a token only Superset can mint, and the decoded
+    claims (not the ``AccessToken`` copy) build the ``GuestUser``.
+
+    Raises:
+        ValueError: If the bearer is not a valid guest token (fail closed).
+    """
+    raw_token = getattr(access_token, "token", None)
+    parsed: Any = None
+    if isinstance(raw_token, str) and raw_token:
+        try:
+            parsed = security_manager.parse_jwt_guest_token(raw_token)
+        except Exception:  # noqa: BLE001 — bad signature / audience / expiry
+            parsed = None
+    if (
+        not isinstance(parsed, dict)
+        or parsed.get("type") != "guest"
+        or parsed.get("user") is None
+        or parsed.get("resources") is None
+        or parsed.get("rls_rules") is None
+        or security_manager._is_guest_token_revoked(parsed)  # noqa: SLF001

Review Comment:
   Fixed in 272e4a61c091eddec8e47caa73fe820e1ffec0a2 (retained at merge head 
124b49628647cc9df2bead1f676ead7fae73e65b). I added `or not parsed.get("exp")` 
and the `exp` parameter to `test_guest_reverification_requires_core_structure`. 
That case failed on the previous head with `DID NOT RAISE ValueError` and 
passes with the guard. 
`test_guest_reverification_rejects_non_guest_type[None/access]` also passes; 
deleting the type condition in a scratch copy makes both cases fail.
   
   Checking the discovery siblings also exposed rejection being mistaken for 
absent authentication in search, and revocation-backend errors being mistaken 
for setup failures. 
`test_guest_reverification_observes_later_dashboard_revocation[tool_search]` 
and all three `test_guest_reverification_backend_failure_is_credential_failure` 
cases failed before those changes and pass after them.
   
   Siblings checked:
   - superset/mcp_service/auth.py:599 — fixed in 
272e4a61c091eddec8e47caa73fe820e1ffec0a2
   - superset/mcp_service/server.py:450 — fixed in 
272e4a61c091eddec8e47caa73fe820e1ffec0a2
   - superset/mcp_service/middleware.py:1282 — not affected: its ValueError 
path already hides all tools; the backend exception is normalized before 
reaching it
   - superset/mcp_service/guest_token_verifier.py:74 — not affected: transport 
already rejects non-guest type, missing exp, and verification/backend exceptions
   - superset/mcp_service/composite_token_verifier.py:266 — not affected: 
delegates guest verification and forwards its result; alternate sources still 
encounter resolution-time re-verification
   - superset/security/manager.py:6264 — not affected: the web guest loader is 
a separate entry point with its own rejection handling; this change aligns MCP 
resolution with the MCP transport contract
   - superset/security/manager.py:6402 — not affected: shared decoding verifies 
a present exp; requiring its presence belongs to the MCP admission checks
   



##########
superset/mcp_service/auth.py:
##########
@@ -665,17 +704,20 @@ def _resolve_user_from_jwt_context(app: Any) -> MCPUser | 
None:  # noqa: C901
             raise ValueError(
                 "Guest-marked token presented but embedded guest auth is not 
enabled"
             )
+        guest_claims = _reverify_guest_bearer(access_token)

Review Comment:
   Yes: I am deliberately keeping the check authoritative at every identity 
resolution, including discovery, rather than trusting transport admission or 
caching by token/session. Your cost observation is correct: 
`SupersetSecurityManager._is_guest_token_revoked_by_embedded` iterates 
dashboard resources, calling `EmbeddedDashboardDAO.find_by_id` and potentially 
`Dashboard.get` for each.
   
   I considered request-scoped caching, but the resolver does not own a 
reliable HTTP-request lifecycle: `_get_app_context_manager` supports 
request-backed calls, fresh per-call contexts without a Flask request, and 
worker-owned contexts; worker dispatch also copies context variables. A 
token/context/session cache is therefore not a cheap substitute for an 
explicitly bounded request cache, and could retain a result across later 
expiry/revocation. I documented the cost/freshness tradeoff rather than 
introducing that lifecycle change here.
   
   Evidence: `test_guest_reverification_observes_later_dashboard_revocation` 
accepts the first resolution, advances a dashboard cutoff, and checks rejection 
on the second resolution (direct, tools/list, and tool search), with exactly 
two DAO lookups and only one GuestUser construction. The search case failed 
before 272e4a61c091eddec8e47caa73fe820e1ffec0a2; the direct/list cases are 
freshness controls. The sibling backend-failure regression also failed before 
that commit. Both discovery paths deny rejected credentials at head 
124b49628647cc9df2bead1f676ead7fae73e65b.
   
   Siblings checked:
   - superset/mcp_service/auth.py:599 — fixed in 
272e4a61c091eddec8e47caa73fe820e1ffec0a2
   - superset/mcp_service/server.py:450 — fixed in 
272e4a61c091eddec8e47caa73fe820e1ffec0a2
   - docs/admin_docs/configuration/mcp-server.mdx:664 — fixed in 
272e4a61c091eddec8e47caa73fe820e1ffec0a2
   - superset/mcp_service/guest_token_verifier.py:74 — not affected: transport 
admission is checked independently, never reused as a cached resolution decision
   - superset/mcp_service/auth.py:1197 — not affected: retains the 
per-call/request/worker context selection contract; no cache was introduced
   - superset/mcp_service/worker.py:505 — not affected: context copying is 
unchanged; no cached guest verification result is carried between calls
   - superset/security/manager.py:6336 — not affected: per-dashboard revocation 
lookup semantics are unchanged
   
   Remaining: no per-request cache is introduced; repeated resolution still 
pays the parse/revocation cost. I am leaving this question open for your 
response.
   



##########
tests/unit_tests/mcp_service/test_multi_auth_client_id.py:
##########
@@ -0,0 +1,589 @@
+# 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.
+
+"""Identity resolution for tokens wrapped by FastMCP's ``MultiAuth``.
+
+FastMCP 4's ``MultiAuth`` rewrites ``AccessToken.client_id`` into a
+source-qualified JSON string and keeps the verifier's own value in
+``original_client_id``. These tests drive real verifier chains
+(``GuestTokenVerifier`` / ``CompositeTokenVerifier`` inside ``MultiAuth``) and
+check that guest and API-key tokens still resolve to the right principal, and
+that a forged guest-marked token still cannot pose as a guest.
+"""
+
+import time
+from collections.abc import Iterator
+from types import SimpleNamespace
+from typing import Any
+from unittest.mock import MagicMock, patch
+
+import jwt
+import pytest
+from fastmcp.server.auth import AccessToken, MultiAuth
+from fastmcp.server.auth.providers.jwt import TokenVerifier
+
+from superset import security_manager
+from superset.app import SupersetApp
+from superset.mcp_service.auth import (
+    _resolve_user_from_api_key,
+    _resolve_user_from_jwt_context,
+)
+from superset.mcp_service.composite_token_verifier import (
+    API_KEY_PASSTHROUGH_CLAIM,
+    API_KEY_VALIDATED_USERNAME_CLAIM,
+    CompositeTokenVerifier,
+)
+from superset.mcp_service.guest_token_verifier import (
+    GUEST_TOKEN_CLAIM,
+    GuestTokenVerifier,
+)
+from superset.mcp_service.mcp_config import default_user_resolver
+
+GUEST_SECRET = "a-strong-guest-token-secret-for-tests"  # noqa: S105
+
+# FastMCP 4's MultiAuth qualifies client_id; FastMCP 3's passes it through.
+MULTI_AUTH_QUALIFIES_CLIENT_ID = "original_client_id" in 
AccessToken.model_fields
+
+
[email protected](autouse=True)
+def isolate_identity_config(app: SupersetApp) -> Iterator[None]:
+    """Restore the module-scoped app's auth configuration after each test."""
+    with patch.dict(app.config):
+        yield
+
+
+class _StubVerifier(TokenVerifier):
+    """A token source that returns a fixed AccessToken for any bearer."""
+
+    def __init__(self, result: AccessToken | None) -> None:
+        super().__init__(base_url=None, required_scopes=[])
+        self._result = result
+
+    async def verify_token(self, token: str) -> AccessToken | None:
+        return self._result
+
+
+def _guest_claims() -> dict[str, Any]:
+    return {
+        "user": {"username": "embed-guest"},
+        "resources": [{"type": "dashboard", "id": "abc-uuid"}],
+        "rls_rules": [],
+        "iat": int(time.time()),
+        "exp": int(time.time()) + 300,
+        "aud": "superset-test",
+        "type": "guest",
+    }
+
+
+def _mint_guest_token(claims: dict[str, Any], secret: str = GUEST_SECRET) -> 
str:
+    return jwt.encode(claims, secret, algorithm="HS256")
+
+
[email protected]
+def guest_app(app: SupersetApp) -> SupersetApp:
+    """Enable embedded guest auth over MCP with a known guest-token secret."""
+    app.config.update(
+        {
+            "MCP_EMBEDDED_GUEST_AUTH_ENABLED": True,
+            "GUEST_TOKEN_JWT_SECRET": GUEST_SECRET,
+            "GUEST_TOKEN_JWT_ALGO": "HS256",
+            "GUEST_TOKEN_JWT_AUDIENCE": "superset-test",
+        }
+    )
+    return app
+
+
+async def _verify_through_chain(
+    app: SupersetApp, raw_token: str, *, multi_auth: bool
+) -> AccessToken | None:
+    """Run ``raw_token`` through the default guest chain, optionally in 
MultiAuth."""
+    composite = CompositeTokenVerifier(
+        jwt_verifier=None,
+        api_key_prefixes=[],
+        app=app,
+        guest_verifier=GuestTokenVerifier(app=app),
+    )
+    verifier: Any = MultiAuth(verifiers=[composite]) if multi_auth else 
composite
+    sm = app.appbuilder.sm
+    with (
+        patch("superset.is_feature_enabled", return_value=True),
+        patch.object(sm, "_is_guest_token_revoked", return_value=False),
+        patch.object(sm, "find_role", return_value=object()),
+    ):
+        return await verifier.verify_token(raw_token)
+
+
+def _resolve_guest(app: SupersetApp, access_token: Any) -> tuple[Any, 
MagicMock]:
+    """Resolve ``access_token`` via the JWT-context path, guest flags on."""
+    fake_guest = MagicMock(name="GuestUser")
+    with (
+        patch(
+            "fastmcp.server.dependencies.get_access_token", 
return_value=access_token
+        ),
+        patch("superset.mcp_service.auth.is_feature_enabled", 
return_value=True),
+        patch.object(security_manager, "_is_guest_token_revoked", 
return_value=False),
+        patch.object(
+            security_manager, "get_guest_user_from_token", 
return_value=fake_guest
+        ) as get_guest_user,
+        patch(
+            "superset.mcp_service.auth.load_user_with_relationships",
+            return_value=None,
+        ),
+    ):
+        return _resolve_user_from_jwt_context(app), get_guest_user
+
+
+# -- guest tokens --
+
+
[email protected]("multi_auth", [False, True], ids=["plain", 
"multi_auth"])
[email protected]
+async def test_guest_token_resolves_as_guest(
+    guest_app: SupersetApp, multi_auth: bool
+) -> None:
+    """A real guest token resolves to a GuestUser whether or not the verifier
+    chain is wrapped in ``MultiAuth`` (which qualifies ``client_id``)."""
+    claims = _guest_claims()
+    access_token = await _verify_through_chain(
+        guest_app, _mint_guest_token(claims), multi_auth=multi_auth
+    )
+    assert access_token is not None
+    if multi_auth and MULTI_AUTH_QUALIFIES_CLIENT_ID:
+        # The precondition for the bug: MultiAuth rewrote client_id.
+        assert access_token.client_id != "guest"
+        assert access_token.original_client_id == "guest"
+
+    result, get_guest_user = _resolve_guest(guest_app, access_token)
+
+    assert result is get_guest_user.return_value
+    passed_claims = get_guest_user.call_args.args[0]
+    assert GUEST_TOKEN_CLAIM not in passed_claims
+    assert passed_claims == claims
+
+
[email protected]("multi_auth", [False, True], ids=["plain", 
"multi_auth"])
[email protected]
+async def test_forged_guest_marker_cannot_pose_as_guest(
+    guest_app: SupersetApp, multi_auth: bool
+) -> None:
+    """A token that carries the guest marker and ``client_id="guest"`` but 
whose
+    bearer is not a guest token signed with ``GUEST_TOKEN_JWT_SECRET`` (e.g. an
+    external IdP JWT returned by a source that does not strip the marker) must
+    be rejected, not resolved as a guest."""
+    forged_claims = _guest_claims()
+    forged_raw = _mint_guest_token(forged_claims, secret="idp-signing-key")  # 
noqa: S106
+    forged = AccessToken(
+        token=forged_raw,
+        client_id="guest",
+        scopes=[],
+        claims={GUEST_TOKEN_CLAIM: True, **forged_claims},
+    )
+    access_token: Any = forged
+    if multi_auth:
+        access_token = await 
MultiAuth(verifiers=[_StubVerifier(forged)]).verify_token(
+            forged_raw
+        )
+        assert access_token is not None
+
+    with (
+        patch(
+            "fastmcp.server.dependencies.get_access_token", 
return_value=access_token
+        ),
+        patch("superset.mcp_service.auth.is_feature_enabled", 
return_value=True),
+        patch.object(security_manager, "get_guest_user_from_token") as 
get_guest_user,
+        pytest.raises(ValueError, match="not a valid guest token"),
+    ):
+        _resolve_user_from_jwt_context(guest_app)
+    get_guest_user.assert_not_called()
+
+
+def test_guest_resolution_uses_verified_claims(guest_app: SupersetApp) -> None:
+    """The GuestUser is built from the re-verified bearer, so claims edited on
+    the AccessToken after verification cannot widen the guest's resources."""
+    claims = _guest_claims()
+    tampered = AccessToken(
+        token=_mint_guest_token(claims),
+        client_id="guest",
+        scopes=[],
+        claims={
+            GUEST_TOKEN_CLAIM: True,
+            **claims,
+            "resources": [{"type": "dashboard", "id": "someone-elses"}],
+        },
+    )
+
+    _, get_guest_user = _resolve_guest(guest_app, tampered)
+
+    assert get_guest_user.call_args.args[0]["resources"] == claims["resources"]
+
+
+# -- API-key tokens --
+
+
[email protected]("multi_auth", [False, True])
[email protected]
+async def test_api_key_fast_path_through_multi_auth(
+    app: SupersetApp, multi_auth: bool
+) -> None:
+    """A transport-validated API key wrapped by ``MultiAuth`` defers from the
+    JWT path and loads the validated user on the API-key path."""
+    composite = CompositeTokenVerifier(
+        jwt_verifier=None, api_key_prefixes=["sst_"], app=app
+    )
+    with patch.object(composite, "_validate_api_key_sync", 
return_value=("alice", [])):
+        verifier: Any = MultiAuth(verifiers=[composite]) if multi_auth else 
composite
+        access_token = await verifier.verify_token("sst_abc123")
+    assert access_token is not None
+    # Editing the JWT-style claims must not alter the FAB-validated identity.
+    access_token.claims[API_KEY_VALIDATED_USERNAME_CLAIM] = "admin"
+    if multi_auth and MULTI_AUTH_QUALIFIES_CLIENT_ID:
+        assert access_token.client_id != "api_key"
+
+    alice = MagicMock(username="alice")
+    app.config["FAB_API_KEY_ENABLED"] = True
+    with (
+        patch(
+            "fastmcp.server.dependencies.get_access_token", 
return_value=access_token
+        ),
+        patch(
+            "superset.mcp_service.auth.load_user_with_relationships",
+            return_value=alice,
+        ) as load_user,
+        patch.object(app.appbuilder.sm, "validate_api_key") as validate,
+    ):
+        assert _resolve_user_from_jwt_context(app) is None
+        assert _resolve_user_from_api_key(app) is alice
+    load_user.assert_called_once_with(username="alice")
+    validate.assert_not_called()
+
+
[email protected]
+async def test_api_key_fallback_through_multi_auth(app: SupersetApp) -> None:
+    """A prefix-only API-key pass-through (no transport validation) wrapped by
+    ``MultiAuth`` is still validated against FAB on the API-key path."""
+    composite = CompositeTokenVerifier(
+        jwt_verifier=None, api_key_prefixes=["sst_"], app=None
+    )
+    access_token = await 
MultiAuth(verifiers=[composite]).verify_token("sst_abc123")
+    assert access_token is not None
+
+    alice = MagicMock(username="alice")
+    app.config["FAB_API_KEY_ENABLED"] = True
+    with (
+        patch(
+            "fastmcp.server.dependencies.get_access_token", 
return_value=access_token
+        ),
+        patch.object(
+            app.appbuilder.sm, "validate_api_key", return_value=alice, 
create=True
+        ) as validate,
+        patch(
+            "superset.mcp_service.auth.load_user_with_relationships",
+            return_value=alice,
+        ),
+    ):
+        assert _resolve_user_from_jwt_context(app) is None
+        assert _resolve_user_from_api_key(app) is alice
+    validate.assert_called_once_with("sst_abc123")
+
+
+# -- default user resolver and helper --
+
+
[email protected]
+async def test_default_user_resolver_uses_unqualified_client_id(
+    app: SupersetApp,
+) -> None:
+    """The last-resort ``client_id`` username fallback sees the verifier's own
+    client id, not ``MultiAuth``'s qualified JSON form."""
+    token = AccessToken(token="t", client_id="svc-bot", scopes=[], claims={})  
# noqa: S106
+    wrapped = await 
MultiAuth(verifiers=[_StubVerifier(token)]).verify_token("t")
+    assert wrapped is not None
+
+    assert default_user_resolver(app, wrapped) == "svc-bot"
+
+
+def test_get_verifier_client_id_without_original_attribute() -> None:
+    """Tokens without ``original_client_id`` (FastMCP 3) use ``client_id``."""
+    from superset.mcp_service.composite_token_verifier import 
get_verifier_client_id
+
+    assert get_verifier_client_id(SimpleNamespace(client_id="guest")) == 
"guest"
+    assert get_verifier_client_id(MagicMock(client_id="guest")) == "guest"
+    assert (
+        get_verifier_client_id(
+            SimpleNamespace(
+                client_id='["src","guest",null,null]', 
original_client_id="guest"
+            )
+        )
+        == "guest"
+    )
+    assert get_verifier_client_id(SimpleNamespace()) is None
+
+
[email protected](
+    "chain", ["plain", "composite", "external_first", "composite_first"]
+)
[email protected]
+async def test_idp_api_key_claims_cannot_select_another_user(
+    app: SupersetApp, chain: str
+) -> None:
+    """Internal API-key claims in a signed IdP JWT are not validation 
evidence."""
+    from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+    secret = "independent-idp-signing-secret-for-tests"  # noqa: S105
+    raw = jwt.encode(
+        {
+            "sub": "alice",
+            "client_id": "api_key",
+            "original_client_id": "api_key",
+            "exp": int(time.time()) + 300,
+            API_KEY_PASSTHROUGH_CLAIM: True,
+            API_KEY_VALIDATED_USERNAME_CLAIM: "admin",
+        },
+        secret,
+        algorithm="HS256",
+    )
+    external = JWTVerifier(public_key=secret, algorithm="HS256")
+    composite = CompositeTokenVerifier(external, ["sst_"], app=app)
+    verifier: Any = external
+    if chain == "composite":
+        verifier = composite
+    elif chain == "external_first":
+        verifier = MultiAuth(verifiers=[external, composite])
+    elif chain == "composite_first":
+        verifier = MultiAuth(verifiers=[composite, external])
+    access_token = await verifier.verify_token(raw)
+    assert access_token is not None
+    app.config["FAB_API_KEY_ENABLED"] = True
+    alice = MagicMock(username="alice")
+    with (
+        patch(
+            "fastmcp.server.dependencies.get_access_token", 
return_value=access_token
+        ),
+        patch.object(
+            app.appbuilder.sm, "validate_api_key", return_value=None
+        ) as validate,
+        patch(
+            "superset.mcp_service.auth.load_user_with_relationships", 
return_value=alice
+        ) as load,
+    ):
+        # A stripped token retains its JWT identity; an unwrapped external
+        # source's forged API-key assertion must fail closed at FAB validation.
+        user = _resolve_user_from_jwt_context(app)
+        if user is None:
+            with pytest.raises(PermissionError, match="Invalid or expired API 
key"):
+                _resolve_user_from_api_key(app)
+            validate.assert_called_once_with(raw)
+            load.assert_not_called()
+        else:
+            assert user is alice
+            load.assert_called_once_with("alice")
+            validate.assert_not_called()
+
+
[email protected]("revocation", ["version", "dashboard"])
[email protected]("chain", ["plain", "guest_first", "external_first"])
[email protected]
+async def test_revoked_guest_cannot_bypass_guest_verifier(
+    guest_app: SupersetApp, chain: str, revocation: str
+) -> None:
+    """A different token source cannot bypass core guest-token revocation."""
+    claims = _guest_claims()
+    raw = _mint_guest_token(claims)
+    marked = AccessToken(
+        token=raw,
+        client_id="guest",
+        scopes=[],
+        claims={GUEST_TOKEN_CLAIM: True, **claims},
+    )
+    verifier: Any = _StubVerifier(marked)
+    if chain != "plain":
+        sources = [GuestTokenVerifier(guest_app), verifier]
+        verifier = MultiAuth(
+            verifiers=sources if chain == "guest_first" else sources[::-1]
+        )
+    guest_app.config["GUEST_TOKEN_REVOCATION_ENABLED"] = revocation == 
"version"
+    cutoff = SimpleNamespace(guest_token_revoked_before=int(time.time()) + 1)
+    with (
+        patch("superset.is_feature_enabled", return_value=True),
+        patch(
+            
"superset.security.manager.get_current_guest_token_revocation_version",
+            return_value=1,
+        ),
+        patch(
+            "superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
+            return_value=cutoff,
+        ),
+        patch.object(guest_app.appbuilder.sm, "find_role", 
return_value=object()),
+    ):
+        access_token = await verifier.verify_token(raw)
+        assert access_token is not None
+        with (
+            patch(
+                "fastmcp.server.dependencies.get_access_token",
+                return_value=access_token,
+            ),
+            patch("superset.mcp_service.auth.is_feature_enabled", 
return_value=True),
+            patch.object(security_manager, "get_guest_user_from_token") as 
build,
+            pytest.raises(ValueError, match="not a valid guest token"),
+        ):
+            _resolve_user_from_jwt_context(guest_app)
+        build.assert_not_called()
+
+
[email protected]("client_id", ["guest", "api_key"])
+def test_crafted_original_client_id_is_not_validation_evidence(
+    guest_app: SupersetApp, client_id: str
+) -> None:
+    """An original_client_id field cannot authorize either internal 
identity."""
+    raw = _mint_guest_token(_guest_claims(), secret="independent-idp-secret")  
# noqa: S106
+    token = SimpleNamespace(
+        token=raw,
+        client_id="external",
+        original_client_id=client_id,
+        claims={
+            GUEST_TOKEN_CLAIM: True,
+            API_KEY_PASSTHROUGH_CLAIM: True,
+            API_KEY_VALIDATED_USERNAME_CLAIM: "admin",
+        },
+    )
+    guest_app.config["FAB_API_KEY_ENABLED"] = True
+    with (
+        patch("fastmcp.server.dependencies.get_access_token", 
return_value=token),
+        patch("superset.mcp_service.auth.is_feature_enabled", 
return_value=True),
+        patch.object(guest_app.appbuilder.sm, "validate_api_key", 
return_value=None),
+        patch("superset.mcp_service.auth.load_user_with_relationships") as 
load,
+        patch.object(security_manager, "get_guest_user_from_token") as build,
+    ):
+        if client_id == "guest":
+            with pytest.raises(ValueError, match="not a valid guest token"):
+                _resolve_user_from_jwt_context(guest_app)
+        else:
+            assert _resolve_user_from_jwt_context(guest_app) is None
+            with pytest.raises(PermissionError, match="Invalid or expired API 
key"):
+                _resolve_user_from_api_key(guest_app)
+        load.assert_not_called()
+        build.assert_not_called()
+
+
[email protected]("missing", ["user", "resources", "rls_rules"])
+def test_guest_reverification_requires_core_structure(
+    guest_app: SupersetApp, missing: str
+) -> None:
+    """Other token sources cannot bypass the embedded flow's structural 
checks."""
+    claims = _guest_claims()
+    del claims[missing]
+    token = AccessToken(
+        token=_mint_guest_token(claims),
+        client_id="guest",
+        scopes=[],
+        claims={GUEST_TOKEN_CLAIM: True, **claims},
+    )
+    with pytest.raises(ValueError, match="not a valid guest token"):
+        _resolve_guest(guest_app, token)
+
+
[email protected]("fallback", ["dev", "middleware"])
[email protected]("multi_auth", [False, True])
[email protected](
+    "disabled_setting", ["FAB_API_KEY_ENABLED", "MCP_API_KEY_ENABLED"]
+)
[email protected]
+async def test_disabled_api_key_assertion_cannot_fall_through_to_dev_user(
+    app: SupersetApp, multi_auth: bool, disabled_setting: str, fallback: str
+) -> None:
+    """An explicit API-key assertion must not degrade to a development 
identity."""
+    from superset.mcp_service.auth import get_user_from_request
+
+    token = AccessToken(
+        token="external-bearer",  # noqa: S106
+        client_id="api_key",
+        scopes=[],
+        claims={
+            API_KEY_PASSTHROUGH_CLAIM: True,
+            API_KEY_VALIDATED_USERNAME_CLAIM: "admin",
+        },
+    )
+    if multi_auth:
+        token = await MultiAuth(verifiers=[_StubVerifier(token)]).verify_token(
+            token.token
+        )
+    app.config.update(
+        FAB_API_KEY_ENABLED=True,
+        MCP_API_KEY_ENABLED=None,
+        MCP_DEV_USERNAME="admin" if fallback == "dev" else None,
+    )
+    app.config[disabled_setting] = False
+    with (
+        patch("superset.mcp_service.auth.g", 
SimpleNamespace(user=MagicMock())),
+        patch("fastmcp.server.dependencies.get_access_token", 
return_value=token),
+        patch("superset.mcp_service.auth.load_user_with_relationships") as 
load,
+        pytest.raises(PermissionError, match="Authentication required"),
+    ):
+        get_user_from_request()
+    load.assert_not_called()
+
+
[email protected]
+async def test_serialized_api_key_token_revalidates_bearer(app: SupersetApp) 
-> None:
+    """Losing the internal token type requires FAB validation, not claim 
trust."""
+    composite = CompositeTokenVerifier(None, ["sst_"], app=app)
+    with patch.object(composite, "_validate_api_key_sync", 
return_value=("alice", [])):
+        verified = await composite.verify_token("sst_abc123")
+    assert verified is not None
+    serialized = AccessToken.model_validate(verified.model_dump())
+    serialized.claims[API_KEY_VALIDATED_USERNAME_CLAIM] = "admin"
+    app.config["FAB_API_KEY_ENABLED"] = True
+    alice = MagicMock(username="alice")
+    with (
+        patch("fastmcp.server.dependencies.get_access_token", 
return_value=serialized),
+        patch.object(
+            app.appbuilder.sm, "validate_api_key", return_value=alice
+        ) as validate,
+        patch(
+            "superset.mcp_service.auth.load_user_with_relationships", 
return_value=alice
+        ) as load,
+    ):
+        assert _resolve_user_from_jwt_context(app) is None
+        assert _resolve_user_from_api_key(app) is alice
+    validate.assert_called_once_with("sst_abc123")
+    load.assert_called_once_with(username="alice")
+
+
[email protected]("entrypoint", ["tool_list", "tool_search"])
+def test_disabled_api_key_assertion_hides_tools(
+    app: SupersetApp, entrypoint: str
+) -> None:
+    """Discovery distinguishes rejected credentials from absent 
authentication."""
+    from superset.mcp_service.middleware import RBACToolVisibilityMiddleware

Review Comment:
   Fixed in 272e4a61c091eddec8e47caa73fe820e1ffec0a2. I moved 
`RBACToolVisibilityMiddleware` to the module-level imports. I reproduced your 
exact two-file order on FastMCP 4.1.2: 1 failed, 70 passed, with 
`test_disabled_api_key_assertion_hides_tools[tool_list]` failing. The same 
two-file command at merge head 124b49628647cc9df2bead1f676ead7fae73e65b passes 
all 80 tests (including the nine additional cases).
   
   Regression command (inside Docker):
   ```bash
   pytest tests/unit_tests/mcp_service/test_auth_user_resolution.py 
tests/unit_tests/mcp_service/test_multi_auth_client_id.py
   ```
   
   Siblings checked:
   - tests/unit_tests/mcp_service/test_multi_auth_client_id.py:56 — fixed in 
272e4a61c091eddec8e47caa73fe820e1ffec0a2
   - tests/unit_tests/mcp_service/test_auth_user_resolution.py:317 — not 
affected: its temporary auth patch is restored; collection imports middleware 
before the patch runs
   - tests/unit_tests/mcp_service/test_middleware.py:52 — not affected: already 
imports RBACToolVisibilityMiddleware at module scope
   - superset/mcp_service/middleware.py:54 — not affected: production binding 
is unchanged; this was test collection/import order
   - superset/mcp_service/worker.py:366 — not affected: the deferred production 
import is safe once test collection binds middleware outside the auth patch
   - superset/mcp_service/server.py:455 — not affected: search imports 
get_user_from_request at invocation time, so it cannot retain that earlier 
temporary patch
   



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