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
+10
View File
@@ -3257,6 +3257,11 @@ export const PairStartedSchema = {
type: 'string',
title: 'Secret'
},
check: {
type: 'string',
title: 'Check',
default: ''
},
expires_in: {
type: 'integer',
title: 'Expires In',
@@ -3317,6 +3322,11 @@ export const PendingDeviceSchema = {
title: 'Request',
default: ''
},
check: {
type: 'string',
title: 'Check',
default: ''
},
device: {
type: 'string',
title: 'Device'
+8 -6
View File
@@ -812,8 +812,9 @@ export class DevicesService {
* All this hands out is a code that means nothing until somebody with an
* account approves it, so the worst an unwelcome caller achieves is an entry
* that expires ten minutes later. Reachable from the internet when this
* instance is enrolled with a portal, which is what the cap and the portal's
* own per-address limits are between.
* instance is enrolled with a portal, which is what the per-address limit,
* the cap and the portal's own limits are between. At the cap the oldest
* request makes room, so a flood delays a device rather than locking it out.
* @param data The data for the request.
* @param data.requestBody
* @returns PairStarted Successful Response
@@ -1016,7 +1017,8 @@ export class DevicesService {
*
* The first report of a touch screen makes the device touch friendly, as
* the switch would have been set by hand. Later ones leave the setting to
* whoever changed it since.
* whoever changed it since, and so does the first report of a device paired
* before pairings were noted: its switch was set by hand already.
* @param data The data for the request.
* @param data.deviceId
* @returns Message Successful Response
@@ -1065,9 +1067,9 @@ export class DevicesService {
*
* The screen hanging there is refused on its next request and goes back to
* showing a pairing code, while its dashboards and their arrangement stay.
*
* A credential the portal minted for a remote screen carries no nonce, so
* this does not reach it. That one is revoked at the hub.
* The same holds for one the portal minted. An older portal minted without
* the nonce, and such a credential stands only while the nonce is still 0,
* so this refuses it too.
* @param data The data for the request.
* @param data.deviceId
* @returns Message Successful Response
+2
View File
@@ -1093,6 +1093,7 @@ export type PairRequest = {
export type PairStarted = {
code: string;
secret: string;
check?: string;
expires_in?: number;
};
@@ -1110,6 +1111,7 @@ export type PairStatus = {
*/
export type PendingDevice = {
request?: string;
check?: string;
device: string;
name?: string;
board?: string;