Device security and robustness (openpanel C0): check code, pnc, lenient report

- POST /devices/pair answers a 2-character `check`, listed by /devices/pending
  and shown beside each waiting entry; five codes a minute per address, and at
  50 waiting the oldest gives way instead of a 429
- portal-minted device tokens are asked for with the device's nonce (`pnc`) and
  held against it, so unpair and delete revoke them; one without `pnc` stands
  only while the nonce is 0
- a device credential acts as the device (`device_principal`), not as the
  approving superuser, so deleting that account unpairs nothing
- the report is validated port by port: unknown enum values read as `unknown`,
  unreadable ports and sections are left out and logged, `config` may name an
  entry of `config_schemas`; 422 only for no schema 1 or no object
- devices.json is parsed once per change (cached by stat), delete drops the
  report and keeps the nonce, a busy recheck runs in the background
- scope and io only from enabled flows (start/stop tells the devices), a port
  bound twice gives one io row
- the Devices tab's saves cancel and hold back device refetches until settled
This commit is contained in:
Melvin Strobl
2026-10-16 19:26:05 +02:00
parent 13d23f6baf
commit c604cfdf3b
21 changed files with 773 additions and 119 deletions
+40 -16
View File
@@ -455,8 +455,9 @@ As a superuser:
4. Open the pairing link on the device. It shows a six-character code.
5. Enter that code under **Pairing**, check the device description, then press
**Pair**. A device asking from your own network also appears in the list
below, with what it says about itself, and **Pair this one** adopts it
without the code.
below, with what it says about itself and a two-character check. The device
shows the same check beside its code. When the two match, **Pair this one**
adopts it without the code.
The screen collects its credential and shows the assigned published dashboards.
Several dashboards get a navigation rail. The page says whether the device is
@@ -466,16 +467,22 @@ since the instance started.
Each save carries the version it was read at. When someone else saved the
device first, the save is refused and the page shows their version.
One address may ask for five codes a minute. At most 50 requests wait at once,
and a new one pushes out the oldest.
A device 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 messages of its own `in` [ports](#the-devices-own-ports). 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 device and
its dashboards. Deleting the device removes the assignment and its access, and
a device added again under the same name does not bring the old credential
back.
A device acts as itself and not as the superuser who paired it, so deleting or
deactivating that account leaves the device paired.
**Unpair screen** revokes the device's credential and keeps the device and its
dashboards. Deleting the device removes the assignment, its access and its
report, and a device added again under the same name does not bring the old
credential back.
The device definitions are stored in the flow repository (`_devices/<id>.json`),
so their history is in git with the flows. The credential generation lives in
@@ -517,8 +524,11 @@ ports it reports and what the published flows wire, each linked to its node.
| type | The payload type, checked like any message's |
| config | A JSON object passed to the device unread, such as `{"pull": "up", "debounce_ms": 50}` |
Only published flows count. A port wired in two of them, or a device node
naming a device that does not exist, is an error on the canvas. Until a device
Only enabled published flows count, so stopping a flow takes its ports out of
the device's `io` and its credential's reach. A port wired in two of them, or a
device node naming a device that does not exist, is an error on the canvas.
Until it is fixed the device follows the first binding of the port, by message
name. Until a device
reports what it has, a port name is free text; once it has, a port it does not
list is a warning and one wired the wrong way round, or to another type, an
error. `int` and `float` pass for each other.
@@ -537,8 +547,17 @@ Home, not a fault, since a panel switched off or asleep breaks nothing.
A paired device says what it has with `PUT /devices/{id}/report`, from its own
credential only. openpanel sends it after pairing and once per boot. The report
replaces the one before, is at most 16 KB and keys this version does not know
are dropped:
replaces the one before and is at most 16 KB. It is read leniently, so a newer
firmware still reports to an older instance:
- keys this version does not know are dropped
- a value it does not know for a display `shape` or a port `direction` reads as
`unknown`, and a `features` value that is not true or false is left out
- a port it cannot read, such as one with an unknown `dtype`, is left out on its
own and named in the server log
Only a report that is not `schema` 1, or not an object at all, is refused with
422.
```json
{
@@ -558,9 +577,12 @@ are dropped:
```
`config` is a JSON schema of the settings the device reads for that port, which
the Device node turns into a form. `modes` lists the directions a port may be
wired in when it may be either, as a GPIO pin. `on_demand` marks a port that
sends only while somebody wants it.
the Device node turns into a form. It may instead name an entry of a top-level
`config_schemas` object, so pins that share a schema send it once:
`"config_schemas": {"pin": {...}}` and `"config": "pin"` on each port. The
schema is put in place when the report is read. `modes` lists the directions a
port may be wired in when it may be either, as a GPIO pin. `on_demand` marks a
port that sends only while somebody wants it.
The device's page shows the report under **Capabilities**, with each port's
binding or **Use in flow** for it. A reported screen becomes a canvas preset in
@@ -591,9 +613,11 @@ a request that came through it, or from outside your network, is adopted by
its code only and never from the list.
The instance enforces the same dashboard and message limits. Remote
credentials are issued by the portal, so **Unpair screen** does not revoke
them; revoke them at the portal. Deleting the device removes access on the
instance.
credentials are issued by the portal with the device's credential generation
in them, so **Unpair screen** and deleting the device revoke them as they do a
local one. A credential from a portal older than this carries none and keeps
working only while the device was never unpaired or deleted. Pair such a
screen again once the portal is updated.
## See also