feat(session): per-owner printer/config storage via cookie-scoped bearer key
Printers and groups (Client) are now scoped to an Owner identified by an opaque
bearer key (secrets.token_urlsafe(32)) stored in an httponly cookie, defaulting
to temporary. First-visit modal offers backup-key download (marks permanent) or
temporary-only choice. /session/restore re-attaches a fresh browser to a saved
key. Every printer-facing route enforces ownership (404 on mismatch, not just
filtering) since printer IDs are sequential ints. Drivers stay global/shared.
On upgrade, pre-existing printer/client rows backfill to a synthetic legacy Owner;
its key is written to {DATA_DIR}/legacy_owner_key.txt for manual restore.
SECURITY: Added Origin/Referer same-origin check on POST /session/restore to
block login-CSRF/session-fixation attacks (cross-site form POST can't re-point
victim's cookie at attacker's Owner without hitting that check first).
Tests: 140 pass (2 deselected: pre-existing locale-flaky, unrelated to this change).
Verified live: modal on first visit, isolation between browsers, backup-key
download and restore flow work end-to-end.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -189,7 +189,7 @@ def test_create_printer_invalid_ip(client: TestClient) -> None:
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_printer_detail_shows_driver(client: TestClient) -> None:
|
||||
def test_printer_detail_shows_driver(client: TestClient, owner) -> None:
|
||||
"""GET /printers/{id} returns 200 with all printer fields and driver name."""
|
||||
from imptune.db.models import Driver, Printer
|
||||
|
||||
@@ -204,6 +204,7 @@ def test_printer_detail_shows_driver(client: TestClient) -> None:
|
||||
ip_address="10.0.1.1",
|
||||
port_name="IP_10_0_1_1",
|
||||
driver=driver_obj,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.get(f"/printers/{printer.id}")
|
||||
@@ -220,7 +221,7 @@ def test_printer_detail_not_found(client: TestClient) -> None:
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_printer_detail_no_driver(client: TestClient) -> None:
|
||||
def test_printer_detail_no_driver(client: TestClient, owner) -> None:
|
||||
"""GET /printers/{id} for printer with no driver returns 200 with 'No driver assigned'."""
|
||||
from imptune.db.models import Printer
|
||||
|
||||
@@ -229,6 +230,7 @@ def test_printer_detail_no_driver(client: TestClient) -> None:
|
||||
ip_address="10.0.1.2",
|
||||
port_name="IP_10_0_1_2",
|
||||
driver=None,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.get(f"/printers/{printer.id}")
|
||||
@@ -311,7 +313,7 @@ def test_printers_library_no_form(client: TestClient) -> None:
|
||||
assert 'action="/printers" method="post"' not in html
|
||||
|
||||
|
||||
def test_patch_printer(client: TestClient) -> None:
|
||||
def test_patch_printer(client: TestClient, owner) -> None:
|
||||
"""PATCH /printers/{id} with updated name returns 200, updated name in response, DB updated."""
|
||||
from imptune.db.models import Printer
|
||||
|
||||
@@ -319,6 +321,7 @@ def test_patch_printer(client: TestClient) -> None:
|
||||
name="Original Name",
|
||||
ip_address="10.0.2.1",
|
||||
port_name="IP_10_0_2_1",
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.patch(
|
||||
@@ -339,18 +342,19 @@ def test_patch_printer_not_found(client: TestClient) -> None:
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_client_detail_returns_200(client: TestClient) -> None:
|
||||
def test_client_detail_returns_200(client: TestClient, owner) -> None:
|
||||
"""GET /clients/{id} returns 200 with client name and assigned printer name."""
|
||||
from imptune.db.models import Client, Printer
|
||||
|
||||
# Create client
|
||||
cl = Client.create(name="Detail Client")
|
||||
cl = Client.create(name="Detail Client", owner=owner)
|
||||
# Create printer assigned to that client
|
||||
Printer.create(
|
||||
name="Client Printer",
|
||||
ip_address="10.0.3.1",
|
||||
port_name="IP_10_0_3_1",
|
||||
client=cl,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.get(f"/clients/{cl.id}")
|
||||
@@ -366,7 +370,7 @@ def test_client_detail_not_found(client: TestClient) -> None:
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_client_links_in_printer_list(client: TestClient) -> None:
|
||||
def test_client_links_in_printer_list(client: TestClient, owner) -> None:
|
||||
"""GET /printers with a printer assigned to a client contains href to client detail."""
|
||||
from imptune.db.models import Client, Printer
|
||||
|
||||
@@ -382,6 +386,7 @@ def test_client_links_in_printer_list(client: TestClient) -> None:
|
||||
ip_address="10.0.4.1",
|
||||
port_name="IP_10_0_4_1",
|
||||
client=cl,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.get("/printers")
|
||||
|
||||
Reference in New Issue
Block a user