[Story-1072] Record-level matter and client RLS

Story 1072Epic None37 files · +981/−42

PR #267 · pramaan-io/pramaan-functions · merged 2026-06-08

Why this matters

One firm, two hundred matters, twenty lawyers — and a junior who sees them all.

Firm membership was never enough. Here's what was leaking.

Query succeeds. Data is wrong. Silence.

The fix — Assign once, show the card, enter only yours.

How it flows

sequenceDiagram
  autonumber
  participant app as App session
  participant rls as RLS predicate
  participant asgn as matter_assignments
  participant row as Matter row
  app->>rls: set current_firm_role
  rls->>asgn: can_read_matter()
  asgn->>rls: assignment row?
  rls-->>row: allow or deny

alembic/.../0077_record_level_matter_client_rls.py + src/domains/matter/entity.py

Methodin → outwhat it does
MatterAssignment(class)the SQLModel table binding a user to a matter as the predicate's source of truth
_create_assignment_table() → Nonecreates the matter_assignments table inside the migration
_create_helpers() → Nonedefines shared can_read_matter and can_read_client predicates reused by every policy
_create_current_firm_role_helper() → Noneregisters the session variable that distinguishes firm admin from member

What changed, file by file

Alembic migration that creates the matter_assignments table, defines shared can_read_matter and can_read_client predicates, registers the current_firm_role session variable, and replaces firm-only policies on matter and client rows with shared predicate calls.

new methodsignature
_replace_policy(policy, table, command, using, check) → None
_create_helpers() → None
_drop_helpers() → None
_create_current_firm_role_helper() → None
_create_assignment_table() → None
_seed_opened_by_assignments() → None
_drop_assignment_table() → None
_upgrade_policies() → None
_downgrade_policies() → None
upgrade() → None
downgrade() → None
New fields: revision : strdown_revision : str | Sequence[str] | Nonebranch_labels : str | Sequence[str] | Nonedepends_on : str | Sequence[str] | None

Defines the MatterAssignment SQLModel entity that records which user is assigned to which matter, serving as the source of truth for the shared RLS predicates.

new classsignature
MatterAssignment(class)
New fields: firm_id : UUIDmatter_id : UUIDuser_id : UUIDassigned_by_user_id : UUID | None

Files changed (37)

File±
alembic/env.py+1
alembic/versions/7d2f4a9b0c13_0077_record_level_matter_client_rls.py ★+480
docs/walkthroughs/story-1072-walkthrough.html+89
src/domains/matter/entity.py ★+66
src/shared/db/rls/_verify.py+5 −3
src/shared/db/rls/app.py ★+6 −2
src/shared/db/rls/firm.py+4 −1
tests/integration/test_agent_approval_request_rls.py+1
tests/integration/test_agent_event_rls.py+1
tests/integration/test_agent_idempotency_rls.py+1
tests/integration/test_agent_prompt_versions_rls.py+4
tests/integration/test_agent_run_rls.py+8
tests/integration/test_app_adversary_reads.py+3
tests/integration/test_app_annotations.py+3
tests/integration/test_app_claim_block_refs.py+3
tests/integration/test_app_claim_rbr_links.py+3
tests/integration/test_app_claims.py+3
tests/integration/test_app_clients.py+2
tests/integration/test_app_document_blocks.py+3
tests/integration/test_app_documents.py+3
tests/integration/test_app_filing_types.py+2
tests/integration/test_app_firm_audit.py+1
tests/integration/test_app_firm_routes.py+1
tests/integration/test_app_forums.py+5
tests/integration/test_app_matter_redesign.py+4
tests/integration/test_app_matter_types.py+3
tests/integration/test_app_matters.py+6
tests/integration/test_app_rbrs.py+3
tests/integration/test_app_seats_and_billing.py+2
tests/integration/test_app_support_grants.py+1
tests/integration/test_app_validation_checks.py+3
tests/integration/test_binder_cache_rls.py+1
tests/integration/test_extraction_claim_promotion.py+2
tests/integration/test_extraction_proposals_rls.py+1
tests/integration/test_rls_isolation.py+253 −34
tests/routes/test_admin_firms.py+3 −2
tests/services/test_firm_service.py+1