Make revoking an agent and locking a dashboard actually revoke and lock
An MCP access token is a stateless JWT good until it expires, so deleting the client row revoked nothing already handed out — on the MCP endpoint or on the REST API, which takes the same token directly. Both doors now look the client up by the `client_id` the token has always carried, so tokens already in circulation are held to it too. A dashboard's `locked` setting stopped the client drawing a control and nothing else; the server took a publish from a panel showing it anyway. It now bounds the panel's write scope, resolved live where a flow drives the flag, exactly as the client resolves it. Reads are untouched — read-only is not blind — and so is a querying chart's request, which is how that tile reads rather than something anyone touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CL9zvnnvcp1mvA8o7impxk
This commit is contained in:
@@ -13,12 +13,14 @@ from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
import jwt
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from mcp.server.auth.provider import AccessToken
|
||||
from mcp.server.auth.settings import AuthSettings
|
||||
from mcp.server.transport_security import TransportSecuritySettings
|
||||
from pydantic import AnyHttpUrl
|
||||
from starlette.applications import Starlette
|
||||
|
||||
from fluksio.api.deps import oauth_client_lives
|
||||
from fluksio.core import security
|
||||
from fluksio.core.config import settings
|
||||
from fluksio.mcp import server
|
||||
@@ -30,11 +32,18 @@ _INTERNAL_BASE = "http://fluksio-mcp.internal"
|
||||
|
||||
|
||||
class _JWTVerifier:
|
||||
"""Accept only tokens minted for the MCP channel.
|
||||
"""Accept only tokens minted for the MCP channel, by agents still registered.
|
||||
|
||||
A perfectly valid browser token is refused: it was issued for a person's
|
||||
session, and honouring it here would make agent traffic indistinguishable
|
||||
from theirs.
|
||||
|
||||
The client row is looked up because the token itself cannot be withdrawn:
|
||||
it is stateless and good until ``MCP_TOKEN_EXPIRE_MINUTES`` runs out, so
|
||||
the registration it names is the thing revoking an agent actually removes.
|
||||
Refusing at the door rather than leaving it to the API the tools call means
|
||||
a revoked agent gets the 401 that sends it back to authorize, instead of a
|
||||
tool listing that works and a tool call that does not.
|
||||
"""
|
||||
|
||||
async def verify_token(self, token: str) -> AccessToken | None:
|
||||
@@ -46,9 +55,17 @@ class _JWTVerifier:
|
||||
if payload.get("mcp") is not True:
|
||||
logger.debug("MCP token rejected: not an MCP-channel token")
|
||||
return None
|
||||
client_id = str(payload.get("client_id", ""))
|
||||
# Off the event loop: the lookup is SQLite, like every other read this
|
||||
# process makes, and the session it opens is blocking.
|
||||
if not await run_in_threadpool(oauth_client_lives, client_id):
|
||||
logger.debug(
|
||||
"MCP token rejected: agent %s is no longer registered", client_id
|
||||
)
|
||||
return None
|
||||
return AccessToken(
|
||||
token=token,
|
||||
client_id=str(payload.get("client_id", "")),
|
||||
client_id=client_id,
|
||||
scopes=[security.MCP_SCOPE],
|
||||
subject=str(payload.get("sub", "")),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user