Story-3385 · functions lane · RFD-MATTER-006 F1
Authority now follows the matter, not the firm
A firm role answers what a person may do in the firm. It does not answer whether that person is staffed on a particular matter. Matter mutations now cross a resource-aware boundary that checks the active assignment for the exact matter in the request.
staffed on matter A + team:staff(A) → allowed
staffed on matter A + team:staff(B) → 403
firm admin + content:write(B) → allowed without a roster row
system/agent context → pass through
The capability boundary
require_matter_capability(matter_id, capability) supports
content:write and team:staff. Human members and
editors need an active firm membership plus a same-firm assignment on that
exact matter. Firm admins retain their explicit bypass. Internal sessions
without current_user_id pass through, matching the existing
matter-detail entry pattern.
The Wave-2 entry gate, can_read_matter, content RLS, assignment
schema, lead eligibility, and vacancy repair behavior are unchanged. This
story supplies the authorization foundation only.
One blanket guard became four precise decisions
| Guard | Decision |
|---|---|
_require_staffed | Any active staffed member/editor, or firm admin, may perform non-destructive team staffing. |
_require_can_remove(target) | Applies the DL15 actor/target ladder to the roster on this matter. |
| lead/destructive guard | Lead counsel or firm admin only; retained for destructive changes and occupied-seat control. |
_require_can_claim_vacant_lead | Requires a vacant seat plus a staffed actor or admin, without changing who may become lead. |
DL15 removal ladder
| Actor on this matter | May remove |
|---|---|
| Member | Other non-lead members |
| Editor | Non-lead members and editors |
| Lead | Any non-lead roster member, including a roster-only firm admin |
| Firm admin | Anyone |
Target evaluation uses the existing protected roster reader, because ordinary assignment RLS intentionally exposes only the caller's own row. An unstaffed caller is rejected before that roster reader is reached.
The route inventory is executable
CI walks FastAPI's resolved dependency graph and inventories every
member-enabled mutation route containing {matter_id}. It fails
when a route has neither capability, and it pins the eight current mutation
routes to their expected content:write or
team:staff guard. A planted unguarded route proves the detector
goes red.
Proof
| Scenario | Evidence |
|---|---|
| S11 / S54 | A member staffed on matter A can mutate A; the same content and team requests against matter B return 403, and B's assignment remains. |
| S24–S30 | The full DL15 member/editor/lead/admin target matrix is exercised against real PostgreSQL RLS. |
| Capability matrix | Staffed member, staffed editor, lead, admin, unstaffed, inactive, cross-matter, cross-firm, and system contexts are covered. |
| Route inventory | The detector was observed red with five live member-enabled routes missing a resource guard, then green after all five were wired. |
| Regression | make test: 6,187 passed, 3 skipped. |