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:
+17
-10
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user