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:
+18
-5
@@ -710,8 +710,10 @@ def doc_row(d):
|
||||
f'<span class="docmeta">{meta}</span></a>')
|
||||
|
||||
@app.get("/documents", response_class=HTMLResponse)
|
||||
def documents_index():
|
||||
groups = documents.listing()
|
||||
def documents_index(request: Request):
|
||||
viewer = auth.current_person(request)
|
||||
units = documents.units_for(viewer)
|
||||
groups = documents.listing(units)
|
||||
if groups:
|
||||
cards = []
|
||||
for cat, rows in groups:
|
||||
@@ -721,6 +723,14 @@ def documents_index():
|
||||
inner = "".join(cards)
|
||||
else:
|
||||
inner = '<div class="noev">Nothing posted here yet. Forms and handouts will land on this page as the program year gets going.</div>'
|
||||
if units:
|
||||
signin_note = ('<div class="wrap" style="padding-top:14px"><div class="note">'
|
||||
'Signed in, so anything posted for families is included above. '
|
||||
'<a href="/account">Your account</a></div></div>')
|
||||
else:
|
||||
signin_note = ('<div class="wrap" style="padding-top:14px"><div class="note">'
|
||||
'Some handouts are posted for registered families only. '
|
||||
'<a href="/login">Sign in</a> if you have an account.</div></div>')
|
||||
body = f"""
|
||||
<div class="calhead"><div class="wrap">
|
||||
<div class="eyebrow gold">Pack & Troop 73</div>
|
||||
@@ -730,12 +740,15 @@ def documents_index():
|
||||
<section style="padding:52px 0 70px"><div class="wrap">{inner}</div></section>
|
||||
<section style="padding:0 0 80px"><div class="wrap"><div class="note">
|
||||
Can't find something, or need a form in another format? Message us on Facebook or catch a leader on a {MEETING_DAY} night.
|
||||
</div></div></section>"""
|
||||
</div></div>{signin_note}</section>"""
|
||||
return page("Forms & Documents · Pack & Troop 73", body, "docs")
|
||||
|
||||
@app.get("/documents/{slug}")
|
||||
def document_file(slug: str):
|
||||
d = documents.find(slug)
|
||||
def document_file(slug: str, request: Request):
|
||||
# A members document the viewer cannot reach is indistinguishable from one
|
||||
# that does not exist. That is deliberate: 404, never 403, because a 403
|
||||
# confirms the document to someone holding nothing but a guessed slug.
|
||||
d = documents.find(slug, documents.units_for(auth.current_person(request)))
|
||||
if d is None:
|
||||
body = """
|
||||
<div class="calhead"><div class="wrap">
|
||||
|
||||
Reference in New Issue
Block a user