From eca3fa0049288019ae82cb459c7d71fde3e4822d Mon Sep 17 00:00:00 2001 From: stroblme Date: Thu, 15 Oct 2026 10:10:44 +0200 Subject: [PATCH] A panel writes only what its dashboards' inputs target It still reads every message its dashboards show. A message a tile only draws, or a setting is bound to, is no longer writable from the panel. --- backend/fluksio/api/deps.py | 30 +++++++++++++------------ backend/fluksio/flow/panels.py | 10 +++++++++ backend/tests/api/routes/test_panels.py | 9 +++++++- docs/interface/dashboards.md | 8 ++++--- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/backend/fluksio/api/deps.py b/backend/fluksio/api/deps.py index d92b336..d3dac28 100644 --- a/backend/fluksio/api/deps.py +++ b/backend/fluksio/api/deps.py @@ -99,14 +99,17 @@ def _locked(defn: DashboardDef, live: dict[str, Any]) -> bool: def _panel_writable(panel_id: str, request: Request) -> set[str]: """The messages this panel may publish to. - The same walk as the read allowlist, minus whatever a locked dashboard - contributes: ``locked`` is a dashboard saying it is there to be looked at, - and until this it stopped only the client drawing the control — the server - took the publish from a screen that asked anyway. + What its dashboards' inputs target, which is narrower than what it may + read: a chart, a reading or a bound setting is something a screen shows, + and showing a message does not entitle it to set one. - Not quite all of it: a querying chart publishes the request it reads by, - and a locked dashboard whose charts cannot ask goes blank rather than - read-only. That request is the exception ``panels.requests_of`` names. + A locked dashboard contributes less still: ``locked`` is a dashboard + saying it is there to be looked at, and until this it stopped only the + client drawing the control — the server took the publish from a screen + that asked anyway. Not nothing, though: a querying chart publishes the + request it reads by, and a locked dashboard whose charts cannot ask goes + blank rather than read-only. That request is the exception + ``panels.requests_of`` names. Union, exactly as the allowlist itself is: a message one dashboard on this panel displays and another controls stays writable, because the unlocked @@ -131,9 +134,7 @@ def _panel_writable(panel_id: str, request: Request) -> set[str]: live = controller.state.get_present(bound) if bound and controller else {} for defn in defns: writable |= ( - panels.requests_of(defn) - if _locked(defn, live) - else panels.messages_of(defn) + panels.requests_of(defn) if _locked(defn, live) else panels.targets_of(defn) ) return writable @@ -174,11 +175,12 @@ def _panel_may(payload: dict[str, Any], request: Request) -> None: Publishing is in the list because a panel cannot be strictly read-only — a control on a panel is the point of putting one there, and a querying chart asks for its window by publishing a request, which is why that request - counts as one of its widget's messages. Bounded to those, though: a screen + counts as one of its widget's targets. Bounded to those, though: a screen on a wall has no business reaching a message no tile on it draws, and the - catalogue behind ``GET /messages/`` is the whole namespace at once. A - dashboard that says it is locked is bounded further still — it entitles a - panel to read every message it names and to publish to none of them but + catalogue behind ``GET /messages/`` is the whole namespace at once. It + reads every message its tiles draw and writes only what its inputs target. + A dashboard that says it is locked is bounded further still — it entitles + a panel to read every message it names and to publish to none of them but the requests its own charts ask by. # ponytail: the panel file and the published dashboards are re-read per diff --git a/backend/fluksio/flow/panels.py b/backend/fluksio/flow/panels.py index 91bc4f4..b573c99 100644 --- a/backend/fluksio/flow/panels.py +++ b/backend/fluksio/flow/panels.py @@ -197,6 +197,16 @@ def requests_of(defn: DashboardDef) -> set[str]: return {w.target for w in defn.widgets if w.type == "chart" and w.target} +def targets_of(defn: DashboardDef) -> set[str]: + """The messages this dashboard's inputs publish to. + + What a screen showing it may write, a narrower set than what it may read. + A tile that only draws a message, and a setting bound to one, are there to + be looked at, so a panel showing them is not entitled to set them. + """ + return {w.target for w in defn.widgets if w.target} + + def io_in(panel_id: str) -> set[str]: """The messages this panel's own hardware publishes to.""" panel = find(panel_id) diff --git a/backend/tests/api/routes/test_panels.py b/backend/tests/api/routes/test_panels.py index bde5a16..7259ef8 100644 --- a/backend/tests/api/routes/test_panels.py +++ b/backend/tests/api/routes/test_panels.py @@ -373,7 +373,8 @@ def test_a_panel_speaks_only_its_own_widgets_messages( == 404 ), target - # And reads the history of everything its widgets bind to. + # And reads the history of everything its widgets bind to, but writes none + # of what they only draw. for name in ( "demo.temperature", "demo.switch_state", @@ -384,6 +385,12 @@ def test_a_panel_speaks_only_its_own_widgets_messages( client.get(f"{messages}/{name}/history", headers=panel_headers).status_code == 200 ), name + assert ( + client.post( + f"{messages}/{name}", headers=panel_headers, json={"value": 1} + ).status_code + == 403 + ), name # A message no tile on this panel names is not its business, in either # direction. diff --git a/docs/interface/dashboards.md b/docs/interface/dashboards.md index 11cf520..ffcf9f5 100644 --- a/docs/interface/dashboards.md +++ b/docs/interface/dashboards.md @@ -389,9 +389,11 @@ As a superuser: The screen collects its credential and shows the assigned published dashboards. Several dashboards get a navigation rail. -A panel credential permits its assigned dashboards, their bound messages and -the artifacts they show. It also permits publishing from its controls and query -charts. On a locked dashboard only the query charts may still publish. +A panel credential reads its assigned dashboards, their bound messages and the +artifacts they show. It writes only the targets of their controls and query +charts, and the `in` ports of its own [I/O](#the-devices-own-io). A message its +tiles only draw, or a dashboard setting is bound to, stays read only. On a +locked dashboard only the query charts may still publish. **Unpair screen** revokes a locally paired credential and keeps the panel's dashboard assignments. Deleting the panel removes the assignment and its