STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
checkout.py
Go to the documentation of this file.
1"""Refuse writes to the live config from someone who does not hold the design.
2
3The design bar already refuses to autosave without the checkout, and
4``/api/engine/documents/{id}/autosave`` already 423s a non-holder
5(``stardesign.documents``). So why gate the config routes too?
6
7Because between those two points sits the session config, and *that* is what the
8UI shows you. Without this, a read-only viewer's edits land in their own session,
9the screen updates, and nothing ever tells them the design did not change --
10the edit simply evaporates at the next reload. Refusing at the point of the edit
11is what turns a silent loss into an error.
12
13This is a correctness guard, not a security boundary. The session config is
14per-user, so a write here can only ever affect the caller's own view; the
15boundary that protects the shared design is the checkout on /autosave.
16
17Which design the caller has open is not something the backend tracks -- the bar
18owns that -- so the client names it, either in the ``X-Design-Owner`` /
19``X-Design-Id`` headers or, for the optimizer layers, in ``design_owner`` /
20``design_id`` query parameters. Both spellings exist because the layer runs are
21consumed with ``EventSource``, which cannot set request headers at all.
22
23When no design is named there is nothing to protect and the write is allowed:
24the config routes long predate designs, and the file-upload and scripted paths
25still use them with no design open at all.
26"""
27
28from __future__ import annotations
29
30from fastapi import Depends, Request
31
32from backend import userdata
33from backend.routers.documents import store
34
35DESIGN_OWNER_HEADER = "X-Design-Owner"
36DESIGN_ID_HEADER = "X-Design-Id"
37DESIGN_OWNER_PARAM = "design_owner"
38DESIGN_ID_PARAM = "design_id"
39
40
41def require_design_checkout(request: Request) -> None:
42 """423 unless the caller holds the checkout on the design they name.
43
44 A no-op when no design is named, or when the named design cannot be found
45 (it was deleted, or the header is stale) -- an unknown design is not
46 somebody else's to protect, and 404-ing a config write on a stale header
47 would be a worse failure than allowing it.
48 """
49 doc_id = request.headers.get(DESIGN_ID_HEADER) or request.query_params.get(DESIGN_ID_PARAM)
50 if not doc_id:
51 return
52
53 viewer = userdata.current_user(request)
54 named_owner = (
55 request.headers.get(DESIGN_OWNER_HEADER)
56 or request.query_params.get(DESIGN_OWNER_PARAM)
57 )
58 owner = userdata.slug_user(named_owner) if named_owner else viewer
59
60 record = store.find_record(owner, doc_id)
61 if record is None:
62 return
63 # Not `require_lock_on`: a hold that lapsed on this caller with nobody
64 # taking it since is taken back rather than refused, as the design's own
65 # autosave does -- see `DesignStore.claim_for_write`.
66 store.claim_for_write(owner, doc_id, viewer)
67
68
69#: Spelled once so the route decorators read as a statement of intent.
70DesignCheckout = Depends(require_design_checkout)
None require_design_checkout(Request request)
Definition checkout.py:41