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:
@@ -24,6 +24,7 @@ from sqlmodel import Session
|
||||
from fluksio.api.deps import decode_token, user_from_token
|
||||
from fluksio.cloud import config as cloud_config
|
||||
from fluksio.core.config import settings
|
||||
from fluksio.main import app
|
||||
from fluksio.models import User
|
||||
from tests.utils.portal import ISSUER, jwks, portal_token
|
||||
from tests.utils.user import create_random_user
|
||||
@@ -290,7 +291,9 @@ def test_a_panel_scoped_portal_token_reaches_only_its_panel(
|
||||
json={"version": version},
|
||||
)
|
||||
|
||||
token = portal_token(portal_key, subject="hallway", scope="panel")
|
||||
# As the hub mints it: with the nonce this instance named when it asked.
|
||||
nonce = app.state.device_store.nonce("hallway")
|
||||
token = portal_token(portal_key, subject="hallway", scope="panel", pnc=nonce)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
assert client.get(f"{devices}/hallway", headers=headers).status_code == 200
|
||||
@@ -312,9 +315,39 @@ def test_a_panel_scoped_portal_token_reaches_only_its_panel(
|
||||
client.get(f"{settings.API_V1_STR}/flows/", headers=headers).status_code == 403
|
||||
)
|
||||
|
||||
# Deleting the device is how the screen is retired, whoever minted its token.
|
||||
# Unpairing refuses it as it refuses one minted here, and so does
|
||||
# deleting the device, which a device created again does not undo.
|
||||
client.post(f"{devices}/hallway/unpair", headers=superuser_token_headers)
|
||||
assert client.get(f"{devices}/hallway", headers=headers).status_code == 401
|
||||
nonce = app.state.device_store.nonce("hallway")
|
||||
token = portal_token(portal_key, subject="hallway", scope="panel", pnc=nonce)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
assert client.get(f"{devices}/hallway", headers=headers).status_code == 200
|
||||
client.delete(f"{devices}/hallway", headers=superuser_token_headers)
|
||||
assert client.get(f"{devices}/hallway", headers=headers).status_code == 401
|
||||
client.post(f"{devices}/hallway", headers=superuser_token_headers)
|
||||
assert client.get(f"{devices}/hallway", headers=headers).status_code == 401
|
||||
client.delete(f"{devices}/hallway", headers=superuser_token_headers)
|
||||
|
||||
|
||||
def test_a_portal_token_from_before_nonces_stands_until_an_unpair(
|
||||
client: TestClient,
|
||||
enrolled: User, # noqa: ARG001 (fixture installs the enrolment)
|
||||
portal_key: rsa.RSAPrivateKey,
|
||||
superuser_token_headers: dict[str, str],
|
||||
) -> None:
|
||||
"""An older hub minted without ``pnc``, which reads as nonce 0."""
|
||||
devices = f"{settings.API_V1_STR}/devices"
|
||||
device_id = f"porch_{uuid.uuid4().hex[:6]}"
|
||||
assert (
|
||||
client.post(f"{devices}/{device_id}", headers=superuser_token_headers)
|
||||
).status_code == 200
|
||||
token = portal_token(portal_key, subject=device_id, scope="panel")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
assert client.get(f"{devices}/{device_id}", headers=headers).status_code == 200
|
||||
client.post(f"{devices}/{device_id}/unpair", headers=superuser_token_headers)
|
||||
assert client.get(f"{devices}/{device_id}", headers=headers).status_code == 401
|
||||
client.delete(f"{devices}/{device_id}", headers=superuser_token_headers)
|
||||
|
||||
|
||||
def test_a_panel_token_naming_no_panel_is_refused(
|
||||
|
||||
Reference in New Issue
Block a user