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:
@@ -345,6 +345,88 @@ def test_a_panel_speaks_only_its_own_widgets_messages(
|
||||
)
|
||||
|
||||
|
||||
def _publish_settings(
|
||||
client: TestClient, headers: dict[str, str], name: str, settings_: dict
|
||||
) -> None:
|
||||
"""Give a published dashboard these settings and publish it again."""
|
||||
saved = client.get(f"{DASHBOARDS}/{name}?draft=true", headers=headers).json()
|
||||
saved["settings"] = settings_
|
||||
written = client.put(f"{DASHBOARDS}/{name}", headers=headers, json=saved)
|
||||
assert written.status_code == 200, written.text
|
||||
published = client.post(
|
||||
f"{DASHBOARDS}/{name}/publish",
|
||||
headers=headers,
|
||||
json={"version": written.json()["version"]},
|
||||
)
|
||||
assert published.status_code == 200, published.text
|
||||
|
||||
|
||||
def test_a_locked_dashboard_entitles_a_panel_to_read_and_not_to_publish(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
"""`locked` bounds what a screen may send, not what it may draw.
|
||||
|
||||
It used to bound neither: the client stopped offering the control and the
|
||||
server took the publish from anything that asked anyway.
|
||||
|
||||
Unions like the allowlist itself, which is what the second dashboard here
|
||||
is for — a message a locked dashboard shows and an unlocked one controls
|
||||
stays writable, because the unlocked one is what entitles the panel to it.
|
||||
"""
|
||||
_dashboard_with(client, superuser_token_headers, "panel_locked", PANEL_WIDGETS)
|
||||
_publish_settings(
|
||||
client, superuser_token_headers, "panel_locked", {"locked": {"value": True}}
|
||||
)
|
||||
_dashboard_with(
|
||||
client,
|
||||
superuser_token_headers,
|
||||
"panel_open",
|
||||
[{"id": "w_button", "type": "button", "config": {"target": "demo.button"}}],
|
||||
)
|
||||
_panels(
|
||||
client,
|
||||
superuser_token_headers,
|
||||
{"panels": [{"id": "foyer", "dashboards": ["panel_locked", "panel_open"]}]},
|
||||
)
|
||||
panel_headers = _pair(client, superuser_token_headers, "foyer")
|
||||
messages = f"{settings.API_V1_STR}/messages"
|
||||
|
||||
# Only the locked dashboard names it, so there is nothing left to send it.
|
||||
assert (
|
||||
client.post(
|
||||
f"{messages}/demo.slider", headers=panel_headers, json={"value": 1}
|
||||
).status_code
|
||||
== 403
|
||||
)
|
||||
# 404, not 403: past the gate, and refused by an engine that knows no such
|
||||
# message. The unlocked dashboard binds this one too.
|
||||
assert (
|
||||
client.post(
|
||||
f"{messages}/demo.button", headers=panel_headers, json={"value": 1}
|
||||
).status_code
|
||||
== 404
|
||||
)
|
||||
# A querying chart's request survives the lock: publishing it is how that
|
||||
# tile *reads*, and a locked dashboard whose charts cannot ask goes blank
|
||||
# rather than read-only.
|
||||
assert (
|
||||
client.post(
|
||||
f"{messages}/demo.query_request",
|
||||
headers=panel_headers,
|
||||
json={"value": {"range_s": 3600, "interval_s": 60}},
|
||||
).status_code
|
||||
== 404
|
||||
)
|
||||
# And what the locked dashboard draws is still readable — read-only is not
|
||||
# blind, and a wall panel showing stale numbers is the failure to avoid.
|
||||
assert (
|
||||
client.get(
|
||||
f"{messages}/demo.temperature/history", headers=panel_headers
|
||||
).status_code
|
||||
== 200
|
||||
)
|
||||
|
||||
|
||||
def test_a_panel_may_read_its_dashboards_own_settings(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
@@ -854,8 +936,9 @@ def test_a_socket_pushes_only_the_frames_it_was_asked_for(tmp_path) -> None:
|
||||
|
||||
store = ArtifactStore(tmp_path / "artifacts")
|
||||
store.volatile = VolatileStore(tmp_path / "ring", limit_bytes=1_000_000)
|
||||
frame = store.put([b"\x89PNG..."], name="f.png", media_type="image/png",
|
||||
volatile=True)
|
||||
frame = store.put(
|
||||
[b"\x89PNG..."], name="f.png", media_type="image/png", volatile=True
|
||||
)
|
||||
kept = store.put([b"checkpoint"], name="w.pt")
|
||||
|
||||
asking = orjson.dumps({"type": "media", "names": ["cam.frame", "other.frame"]})
|
||||
|
||||
Reference in New Issue
Block a user