- 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 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
56 lines
1.5 KiB
Python
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"},
|
|
)
|