Server-side matter-detail entry gate
An unstaffed firm editor could open the detail page of any matter in the firm. This story adds a single server-side authorization gate on the matter-detail entry read so that only a firm admin or a user staffed on the matter can open it. Content visibility is unchanged: editors still see every matter in the register list, in the API, and in the settings audit log. The only boundary added is a workflow gate on entering a matter's detail page.
Before and After
| Caller | Before | After |
|---|---|---|
| Unstaffed firm editor | Opens the matter detail (200) | Refused (403 pramaan.matter.not_on_team); the app shows
a modal and returns to the register list |
| Firm admin | Opens the matter detail (200) | Unchanged — opens (200) |
| Staffed editor or member | Opens the matter detail (200) | Unchanged — opens (200) |
| Register list read | Editor sees all matters | Unchanged — editor still sees all matters |
| Internal / agent read | Reads the matter | Unchanged — not subject to the human-staffing gate |
What Changed
The gate lives in MatterService.get_matter, the top-level read
the app's matter-detail route depends on. It is reached only by the two
app-facing endpoints below, so gating the shared service method covers the
whole entry surface in one place.
GET /app/matters/{matter_id} detail entry
GET /app/matters/{matter_id}/record/bootstrap record page (wraps get_matter)
get_matter(matter_id):
matter = load matter (RLS-visible) # unchanged; hidden -> 404 first
require_matter_detail_entry(matter) # NEW gate
... build and return the detail view
require_matter_detail_entry(matter):
if no app-user principal: return # internal / agent / worker
if firm_admin: return
if staffed on the matter: return # matter_assignments row
raise PramaanForbiddenError(
code = "pramaan.matter.not_on_team", # http 403
)
Staffed Means Editor or Member
"Staffed" is any business.matter_assignments row for the
current user and this matter — a staffed editor and a staffed member
both pass. The gate reuses the existing staffing lookup
(_assignment_role) and the existing
PramaanForbiddenError; it introduces no new primitive.
Only the App User Is Gated
The gate keys off the authenticated app-user principal
(app.current_user_id). Internal, agent, service, and worker
sessions (framer, deep-read, outbox, context-embed, cross-matter
references, agent runs) set only the firm and role context and never a
user id, so the gate returns early and those reads pass straight through.
They are system contexts, not a human opening a workspace. This mirrors
the existing off-team-view audit path, which distinguishes an app-user
read from a system read the same way.
What Stays The Same
No change to business.can_read_matter or any content-table
row-level security — this is an application-layer entry check, not a
content-hiding change. The matters list
(GET /app/matters) is untouched, so editors keep seeing all
matters in the register. A matter that row-level security already hides is
still a 404 (the load runs before the gate), not a 403.