Restart safety: transient credential checks are 1013/503, not a refusal
- A device credential that cannot be checked now (device store not running, nonces unreadable) raises DeviceUnavailable: 503 over HTTP, and the /flows/ws handshake and the tunnel stream accept and close with 1013. 1008 (a 403 before accepting) stays for a revoked or invalid one - Device files are written through a temporary file, so a read mid-save no longer looks like a deleted device - The first report turns Touch friendly on only for a device paired since pairings are noted (DeviceState.paired_at), so devices paired before keep the switch they were given - NodeDef reads `device`/`device_policy` from older clients as the worker label, for a release - Tests: transient vs revoked socket, the touch default, the old worker keys, and a restart with a device node and a node_health rule sending no push
This commit is contained in:
@@ -19,7 +19,7 @@ from fluksio.flow import devices
|
||||
from fluksio.flow.artifacts import is_reference
|
||||
from fluksio.flow.controller import FlowController
|
||||
from fluksio.flow.dashboards import DashboardDef, DashboardStore
|
||||
from fluksio.flow.devices import DeviceDef, DeviceStore, presence
|
||||
from fluksio.flow.devices import DeviceDef, DeviceStore, DeviceUnavailable, presence
|
||||
from fluksio.flow.workers import PythonWorkerPool
|
||||
from fluksio.models import OAuthClient, TokenPayload, User, WorkerToken
|
||||
|
||||
@@ -59,12 +59,15 @@ def _device_for(payload: dict[str, Any], app: Any) -> DeviceDef:
|
||||
|
||||
The claim is still called ``panel``: tokens in circulation say it, and
|
||||
nothing pairs again for a rename.
|
||||
|
||||
What cannot be checked right now raises ``DeviceUnavailable`` rather than
|
||||
refusing: a device told its credential is refused forgets it.
|
||||
"""
|
||||
store: DeviceStore | None = (
|
||||
getattr(app.state, "device_store", None) if app else None
|
||||
)
|
||||
if app is None:
|
||||
raise InvalidTokenError("Nothing to check a device credential against")
|
||||
store: DeviceStore | None = getattr(app.state, "device_store", None)
|
||||
if store is None:
|
||||
raise InvalidTokenError("The device store is not running")
|
||||
raise DeviceUnavailable("The device store is not running")
|
||||
device = store.find(str(payload.get("panel", "")))
|
||||
if device is None:
|
||||
raise InvalidTokenError("This device no longer exists")
|
||||
@@ -72,7 +75,7 @@ def _device_for(payload: dict[str, Any], app: Any) -> DeviceDef:
|
||||
try:
|
||||
nonce = store.nonce(device.id)
|
||||
except (OSError, ValueError) as exc:
|
||||
raise InvalidTokenError(f"The device nonces are unreadable: {exc}")
|
||||
raise DeviceUnavailable(f"The device nonces are unreadable: {exc}")
|
||||
if int(payload.get("pnc") or 0) != nonce:
|
||||
raise InvalidTokenError("This device was paired again")
|
||||
presence.seen(device.id)
|
||||
|
||||
@@ -625,7 +625,8 @@ async def put_report(device_id: str, request: Request, store: DeviceStoreDep) ->
|
||||
|
||||
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.
|
||||
"""
|
||||
device: DeviceDef | None = getattr(request.state, "device", None)
|
||||
if device is None or device.id != device_id:
|
||||
@@ -650,7 +651,13 @@ async def put_report(device_id: str, request: Request, store: DeviceStoreDep) ->
|
||||
),
|
||||
)
|
||||
first = await run_in_threadpool(store.report, device_id, report)
|
||||
if first and report.display and report.display.touch and not device.touch:
|
||||
if (
|
||||
first
|
||||
and report.display
|
||||
and report.display.touch
|
||||
and not device.touch
|
||||
and (await run_in_threadpool(store.state, device_id)).paired_at
|
||||
):
|
||||
with suppress(StaleVersion, DeviceNotFound):
|
||||
await run_in_threadpool(
|
||||
store.write,
|
||||
@@ -724,6 +731,7 @@ def _adopt(
|
||||
)
|
||||
entry.device_id = device_id
|
||||
_pending.save(code, entry)
|
||||
store.paired(device_id)
|
||||
return f"Paired {entry.name or entry.device} with {device_id}"
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The flow API. Everything the editor can do is available here first."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -68,6 +69,8 @@ from fluksio.flow.store import (
|
||||
)
|
||||
from fluksio.models import Flavor, Message, Run
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/flows", tags=["flows"], dependencies=[Depends(get_current_user)]
|
||||
)
|
||||
@@ -1221,15 +1224,25 @@ async def flow_events(websocket: WebSocket, ticket: str = "", token: str = "") -
|
||||
# every other socket and every request is sharing.
|
||||
def _authenticate() -> Any:
|
||||
with Session(engine) as session:
|
||||
return user_from_token(session, token, app=websocket.app)
|
||||
user = user_from_token(session, token, app=websocket.app)
|
||||
only = panel_scope(token, websocket.app) if user is not None else None
|
||||
# A device is online for as long as this socket is open.
|
||||
device = device_of(token, websocket.app) if only is not None else ""
|
||||
return user, only, device
|
||||
|
||||
user = await run_in_threadpool(_authenticate)
|
||||
try:
|
||||
user, only, device = await run_in_threadpool(_authenticate)
|
||||
except Exception as exc:
|
||||
# Not a refusal: the database or the device store could not say. A
|
||||
# device takes 1008, or a 403 before accepting, as its credential
|
||||
# being gone and checks it, so this one is told to try again later.
|
||||
logger.warning("Could not check a socket's credential: %s", exc)
|
||||
await websocket.accept()
|
||||
await websocket.close(code=1013)
|
||||
return
|
||||
if user is None:
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
only = panel_scope(token, websocket.app)
|
||||
# A device is online for as long as this socket is open.
|
||||
device = device_of(token, websocket.app) if only is not None else ""
|
||||
# Nothing until a client says it is drawing something: bytes are what this
|
||||
# socket cannot afford to send speculatively.
|
||||
wanted: set[str] = set()
|
||||
|
||||
Reference in New Issue
Block a user