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:
2026-08-04 11:29:43 +02:00
co-authored by Claude Haiku 4.5
parent 9e46fee312
commit ed41f7f520
27 changed files with 583 additions and 102 deletions
+17 -10
View File
@@ -17,12 +17,16 @@ templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templa
def dashboard(request: Request):
from imptune.db.models import Printer
owner = request.state.owner
recent_printers = list(
Printer.select().order_by(Printer.created_at.desc()).limit(5)
Printer.select()
.where(Printer.owner == owner)
.order_by(Printer.created_at.desc())
.limit(5)
)
recent_packages = list(
Printer.select()
.where(Printer.driver.is_null(False))
.where((Printer.owner == owner) & Printer.driver.is_null(False))
.order_by(Printer.created_at.desc())
.limit(5)
)
@@ -56,9 +60,11 @@ def drivers_page(request: Request):
def printers_page(request: Request):
from imptune.db.models import Client, Driver, Printer
owner = request.state.owner
query = (
Printer.select(Printer, Client)
.join(Client, JOIN.LEFT_OUTER)
.where(Printer.owner == owner)
.order_by(Client.name, Printer.name)
)
grouped: dict[str, list] = defaultdict(list)
@@ -66,7 +72,7 @@ def printers_page(request: Request):
client_name = p.client.name if p.client_id else "Unassigned"
grouped[client_name].append(p)
clients = list(Client.select().order_by(Client.name))
clients = list(Client.select().where(Client.owner == owner).order_by(Client.name))
all_drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
driver_data = []
@@ -89,7 +95,7 @@ def printers_page(request: Request):
def printers_new_page(request: Request):
from imptune.db.models import Client, Driver
clients = list(Client.select().order_by(Client.name))
clients = list(Client.select().where(Client.owner == request.state.owner).order_by(Client.name))
all_drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
driver_data = [
{"driver": d, "names": json.loads(d.driver_desc) if d.driver_desc else []}
@@ -111,7 +117,7 @@ def printer_detail(request: Request, printer_id: int):
.join(Client, JOIN.LEFT_OUTER)
.switch(Printer)
.join(Driver, JOIN.LEFT_OUTER)
.where(Printer.id == printer_id)
.where((Printer.id == printer_id) & (Printer.owner == request.state.owner))
.first()
)
if printer is None:
@@ -148,7 +154,7 @@ def printer_detail(request: Request, printer_id: int):
def clients_page(request: Request):
from imptune.db.models import Client
clients = list(Client.select().order_by(Client.name))
clients = list(Client.select().where(Client.owner == request.state.owner).order_by(Client.name))
return templates.TemplateResponse(
request=request,
name="clients.html",
@@ -160,7 +166,8 @@ def clients_page(request: Request):
def client_detail(request: Request, client_id: int):
from imptune.db.models import Client, Driver, Printer
client = Client.get_or_none(Client.id == client_id)
owner = request.state.owner
client = Client.get_or_none((Client.id == client_id) & (Client.owner == owner))
if client is None:
return HTMLResponse(
content="<h1>404 Not Found</h1><p>Client not found.</p>",
@@ -170,12 +177,12 @@ def client_detail(request: Request, client_id: int):
query = (
Printer.select(Printer, Client)
.join(Client, JOIN.LEFT_OUTER)
.where(Printer.client == client_id)
.where((Printer.client == client_id) & (Printer.owner == owner))
.order_by(Printer.name)
)
grouped = {client.name: list(query)}
clients = list(Client.select().order_by(Client.name))
clients = list(Client.select().where(Client.owner == owner).order_by(Client.name))
all_drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
driver_data = [
{"driver": d, "names": json.loads(d.driver_desc) if d.driver_desc else []}
@@ -203,7 +210,7 @@ def packages_page(request: Request):
.join(Client, JOIN.LEFT_OUTER)
.switch(Printer)
.join(Driver, JOIN.LEFT_OUTER)
.where(Printer.driver.is_null(False))
.where((Printer.owner == request.state.owner) & Printer.driver.is_null(False))
.order_by(Printer.name)
)
return templates.TemplateResponse(