P1: gate members documents per unit, session auth on the admin API
documents.visible() now takes the viewer's unit set. A members document is served and listed only to a signed-in member of the matching unit; 'both' reaches any member; owner and admin reach everything including unit types added later. Everyone else gets 404, never 403. The gate moved INSIDE find(), so there is one path from a slug to a file and no route can forget to check. listed() and visible() stay separate functions. admin_api takes a session first and falls back to X-Admin-Token as break glass. Still fails closed: no session and no ADMIN_TOKEN is 503. Capability, not role, decides per route. announcements.created_by now comes from the session and ignores any value in the request body. tests/smoke_documents.py, 24 checks, including the invariant that the index can never list something serving would refuse.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
smoke_documents.py - the members document gate, per unit.
|
||||
|
||||
Builds a throwaway /docs tree and manifest, then checks who can SEE and who can
|
||||
LIST each document. Runs in-process, stdlib only.
|
||||
|
||||
python3 tests/smoke_documents.py
|
||||
|
||||
The rules under test are the ones that fail silently: a members document must
|
||||
be unservable AND unlisted to the wrong viewer, unlisted must stay servable but
|
||||
off the index, and listing must never be able to show something serving would
|
||||
refuse.
|
||||
"""
|
||||
|
||||
import json, os, sys, tempfile
|
||||
|
||||
TMP = tempfile.mkdtemp()
|
||||
DOCS = os.path.join(TMP, "docs"); os.makedirs(DOCS)
|
||||
os.environ["DOCS_DIR"] = DOCS
|
||||
os.environ["STORE_DB"] = os.path.join(TMP, "smoke.db")
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "app"))
|
||||
|
||||
FILES = ["open.pdf", "hidden.pdf", "packonly.pdf", "trooponly.pdf", "bothunits.pdf"]
|
||||
for f in FILES:
|
||||
open(os.path.join(DOCS, f), "w").write("x")
|
||||
json.dump({
|
||||
"categories": [{"id": "forms", "title": "Forms", "blurb": ""}],
|
||||
"documents": [
|
||||
{"slug": "open", "file": "open.pdf", "title": "Open", "category": "forms",
|
||||
"visibility": "public", "unit": "both"},
|
||||
{"slug": "hidden", "file": "hidden.pdf", "title": "Hidden", "category": "forms",
|
||||
"visibility": "unlisted", "unit": "both"},
|
||||
{"slug": "packonly", "file": "packonly.pdf", "title": "Pack only", "category": "forms",
|
||||
"visibility": "members", "unit": "pack"},
|
||||
{"slug": "trooponly", "file": "trooponly.pdf", "title": "Troop only", "category": "forms",
|
||||
"visibility": "members", "unit": "troop"},
|
||||
{"slug": "bothunits", "file": "bothunits.pdf", "title": "Both", "category": "forms",
|
||||
"visibility": "members", "unit": "both"},
|
||||
]}, open(os.path.join(DOCS, "manifest.json"), "w"))
|
||||
|
||||
import documents as D
|
||||
|
||||
PASS = FAIL = 0
|
||||
|
||||
|
||||
def check(label, cond):
|
||||
global PASS, FAIL
|
||||
if cond:
|
||||
PASS += 1; print(" ok %s" % label)
|
||||
else:
|
||||
FAIL += 1; print(" FAIL %s" % label)
|
||||
|
||||
|
||||
def served(units):
|
||||
return {s for s in ("open", "hidden", "packonly", "trooponly", "bothunits")
|
||||
if D.find(s, units) is not None}
|
||||
|
||||
|
||||
def listed(units):
|
||||
return {d["slug"] for _, rows in D.listing(units) for d in rows}
|
||||
|
||||
|
||||
anon = frozenset()
|
||||
pack = D.units_for({"global_role": None,
|
||||
"memberships": [{"unit_type": "pack", "role": "member"}]})
|
||||
troop = D.units_for({"global_role": None,
|
||||
"memberships": [{"unit_type": "troop", "role": "leader"}]})
|
||||
both = D.units_for({"global_role": None,
|
||||
"memberships": [{"unit_type": "pack", "role": "member"},
|
||||
{"unit_type": "troop", "role": "leader"}]})
|
||||
admin = D.units_for({"global_role": "admin", "memberships": []})
|
||||
crew = D.units_for({"global_role": None,
|
||||
"memberships": [{"unit_type": "crew", "role": "leader"}]})
|
||||
off = D.units_for({"global_role": "owner", "memberships": [], "disabled_at": "2026-01-01T00:00:00+00:00"})
|
||||
|
||||
print("units_for")
|
||||
check("anonymous gets nothing", D.units_for(None) == frozenset())
|
||||
check("pack member -> {pack}", pack == frozenset({"pack"}))
|
||||
check("admin -> every unit type", admin == frozenset(D.DOC_UNITS))
|
||||
check("crew maps to nothing (manifest has no crew)", crew == frozenset())
|
||||
check("disabled owner gets nothing", off == frozenset())
|
||||
|
||||
print("\nserving")
|
||||
check("anon: public and unlisted only", served(anon) == {"open", "hidden"})
|
||||
check("pack member: + pack and both", served(pack) == {"open", "hidden", "packonly", "bothunits"})
|
||||
check("troop leader: + troop and both", served(troop) == {"open", "hidden", "trooponly", "bothunits"})
|
||||
check("in both units: everything", served(both) == set(FILES and
|
||||
{"open", "hidden", "packonly", "trooponly", "bothunits"}))
|
||||
check("admin: everything", served(admin) == {"open", "hidden", "packonly", "trooponly", "bothunits"})
|
||||
check("pack member cannot reach troop doc", D.find("trooponly", pack) is None)
|
||||
check("crew leader reaches no members doc", served(crew) == {"open", "hidden"})
|
||||
|
||||
print("\nlisting")
|
||||
check("anon index is public only", listed(anon) == {"open"})
|
||||
check("unlisted never listed, for anyone", "hidden" not in listed(admin))
|
||||
check("pack member index", listed(pack) == {"open", "packonly", "bothunits"})
|
||||
check("troop leader index", listed(troop) == {"open", "trooponly", "bothunits"})
|
||||
check("admin index", listed(admin) == {"open", "packonly", "trooponly", "bothunits"})
|
||||
|
||||
print("\nlisting can never exceed serving")
|
||||
for name, u in (("anon", anon), ("pack", pack), ("troop", troop), ("admin", admin), ("crew", crew)):
|
||||
check("%s: listed is a subset of served" % name, listed(u) <= served(u))
|
||||
|
||||
print("\nmissing file on disk")
|
||||
os.remove(os.path.join(DOCS, "packonly.pdf"))
|
||||
D._cache["key"] = None
|
||||
check("a members doc with no file is not served", D.find("packonly", pack) is None)
|
||||
check("and not listed", "packonly" not in listed(pack))
|
||||
|
||||
print("\n%d passed, %d failed" % (PASS, FAIL))
|
||||
sys.exit(1 if FAIL else 0)
|
||||
Reference in New Issue
Block a user