Files
app/backend/tests/utils/portal.py
T
Melvin Strobl c604cfdf3b 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
2026-10-16 19:26:05 +02:00

56 lines
1.5 KiB
Python

"""A stand-in portal: its key, its JWKS, and the tokens it would mint.
Shared by the remote-access tests and the panel ones, since a screen paired
through a portal is a portal token that happens to name a panel.
"""
from __future__ import annotations
import json
from datetime import UTC, datetime, timedelta
from typing import Any
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
INSTANCE_ID = "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f"
ISSUER = "https://hub.example.test"
def jwks(key: rsa.RSAPrivateKey) -> dict[str, Any]:
jwk = json.loads(jwt.algorithms.RSAAlgorithm.to_jwk(key.public_key()))
jwk.update({"use": "sig", "alg": "RS256", "kid": "test-portal"})
return {"keys": [jwk]}
def portal_token(
key: rsa.RSAPrivateKey,
*,
subject: str = "portal-user-1",
audience: str = INSTANCE_ID,
issuer: str = ISSUER,
scope: str = "proxy",
**claims: Any,
) -> str:
now = datetime.now(UTC)
pem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
return jwt.encode(
{
"sub": subject,
"iss": issuer,
"aud": audience,
"iat": now,
"exp": now + timedelta(hours=1),
"scope": scope,
**claims,
},
pem,
algorithm="RS256",
headers={"kid": "test-portal"},
)