whoami returns enough to attribute and scope, and CAPS gains finance
scout-finance validates its session against this API and needs four things whoami did not return. Without them it reports itself down rather than degrading, which is correct and also useless. - id, so a finance row can carry entered_by. The email is display-facing and is the wrong thing to write rows against. - preferred_name / full_name, for entered_by_name, captured at write time so a historical report carries the name as of that date. - global_capabilities, separate from the union. The union answers "may they see this screen"; the site-wide set answers "does this grant reach a unit they hold no membership in". For an admin who is also a den leader those are not the same, and collapsing them lets a pack-only grant travel to the troop. - memberships[].capabilities, so a separate service scopes per unit without keeping a second copy of CAPS. Nothing outside this file may map a role to a capability. Built in identity.whoami_payload() rather than in the route, so it is testable with no HTTP and the capability map stays in one place. An API key narrows the per-membership sets too, so a key can never appear to hold what can() would refuse. CAPS: finance:read and finance:write on leader, because a treasurer is a leader and leader-wide read is a deliberate design decision in finance.md. finance:admin on admin only, for categories, accounts and finance settings, which are site-wide. The break-glass token path keeps the same shape with a null id and no memberships. It has no person behind it, so nothing it did could be attributed; scout-finance refuses it outright. Additive throughout. scout-control reads none of these fields. smoke_identity 138, smoke_admin 164, smoke_documents 24. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AHy2gB4QvKmRurfXwYbwCB
This commit is contained in:
+7
-4
@@ -490,15 +490,18 @@ def whoami(request: Request, x_admin_token: str = Header(None)):
|
||||
and identity.get_setting("api_keys_from") == "lan":
|
||||
raise HTTPException(403, "API keys may only be used from the LAN right now "
|
||||
"(site setting api_keys_from)")
|
||||
return {"email": person["email"], "global_role": person.get("global_role"),
|
||||
"capabilities": person["capabilities"], "lan": client_is_lan(request),
|
||||
"via": ("key " + person["key_prefix"]) if person.get("key_scopes") is not None else "session"}
|
||||
via = ("key " + person["key_prefix"]) if person.get("key_scopes") is not None else "session"
|
||||
return identity.whoami_payload(person, lan=client_is_lan(request), via=via)
|
||||
if not ADMIN_TOKEN:
|
||||
raise HTTPException(503, "admin API disabled: sign in, or set ADMIN_TOKEN")
|
||||
if x_admin_token and hmac.compare_digest(x_admin_token, ADMIN_TOKEN):
|
||||
if not client_is_lan(request):
|
||||
raise HTTPException(403, "the admin token is LAN-only")
|
||||
return {"email": None, "via": "admin-token", "capabilities": ["*"], "lan": True}
|
||||
# Break glass has no person behind it, so id is null and there are no
|
||||
# memberships. A caller that needs to attribute a write must refuse
|
||||
# this, and scout-finance does.
|
||||
return {"id": None, "email": None, "via": "admin-token", "capabilities": ["*"],
|
||||
"global_capabilities": ["*"], "memberships": [], "lan": True}
|
||||
raise HTTPException(401, "sign in, send a Bearer key, or a valid X-Admin-Token")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user