Appearance
Trust boundaries
Status: slice 34 · October 2026.
Who is allowed to do what, where that is enforced, and which browser cookies and sessions belong to which product. Security controls themselves are catalogued on Security; this page defines the boundaries they protect.
1. Tenant vs platform perimeter
text
┌─ Tenant perimeter ─────────────────────────────┐
│ Dossiers, Documents, cells, contradictions, │
│ market book, portfolio, Q&A, alerts │
│ Isolation: EF query filters + FORCE RLS GUCs │
└──────────────────────────────────────────────────┘
┌─ Platform perimeter ───────────────────────────┐
│ LegalInstruments / Editions / Provisions / │
│ SourceArtifacts / ReviewItems / Applicability │
│ Writes: PLATFORM_OPERATOR (system:manage) │
│ Tenant reads: resolve + open-statute (hash OK) │
└──────────────────────────────────────────────────┘| Tenant perimeter | Platform perimeter | |
|---|---|---|
| Owner | One institution (OrganizationId) | Locus platform operators |
| Tables | Dossiers + children, market/portfolio/alerts/search, AuthSessions, AuditEvents, … | RegulatorySources, LegalInstruments, LegalEditions, LegalProvisions, LegalSourceArtifacts, LegalReviewItems, LegalIngestionLogs, LegalAmendments, LegalProvisionApplicability (no OrganizationId) |
| Documents | Deal-room PDFs in R2 under tenants/{tenantId}/…, AES-256-GCM | Statutory PDFs in R2 or the local vault dir, SHA-256 verified on read |
| Written by | Tenant roles per the matrix below | PLATFORM_OPERATOR through locus-ops only |
| Read by tenants | Own rows only | regulatory:read → resolve + open-statute |
Tenant uploads never become legal authority for other tenants. Deal-room PDFs and the platform vault are two separate libraries (What Locus is).
Cross-perimeter rules:
PLATFORM_OPERATORhas no dossier-origination or committee rights, and cannot be invited into a tenant through the Members API.- A tenant admin cannot write platform legal tables or ingest market data for another org; the SoR rejects it regardless of what a client sends.
- The one deliberate cross-tenant write path is operator market ingest with an explicit
organizationId(Market ingest (ops)); it is audit-logged asMARKET_INGESTED.
2. RBAC matrix
Six-tier institutional matrix
The permission matrix spans institutional roles at the API and service layer:
| Role | Scope | Permitted operations | Restricted / prohibited actions |
|---|---|---|---|
PLATFORM_OPERATOR | Global platform | System health, tenant provisioning, global catalogs (legal vault), cross-tenant audit maintenance | Direct client deal origination, internal credit committee votes |
INSTITUTION_ADMIN | Tenant organization | User lifecycle, seat allocation, edition customization, module toggling, audit trails | Cross-tenant queries, unilateral regulatory visa issuance |
ARRANGER_LEAD | Deal mandate | Dossier creation, term sheet editing, placement memo sign-off, external sharing | Global platform administration, bypass of accounting checks |
CREDIT_ANALYST | Financial & quant | SYSCOHADA table reconciliation, covenant stress tests, cell overrides with rationale | Memo sign-off (403), user management (403) |
COMPLIANCE_OFFICER | Regulatory & legal | COSUMAF pre-visa audit, legal gazette cross-checks, quitus verification, committee review | Financial cell override (403), covenant margin alteration |
SYNDICATE_GUEST | Read-only deal room | Approved document inspection, sanitized Q&A submission | Memo generation (403), internal credit committee deliberations (403), exports |
Roles beyond the six tiers
The shipped matrix in locus-backend/Services/InstitutionalAuthorization.cs (mirrored in locus-web/src/domains/auth/types.ts) also defines seven specialist tenant roles that map onto the same permission vocabulary: ORIGINATION_BANKER, RISK_MANAGER, LEGAL_COUNSEL, RESEARCH_ANALYST, PORTFOLIO_MANAGER, COMMITTEE_VIEWER, plus the soft aliases administrator and investment_banker used by organization profile data. Treat the table above as the conceptual model and the C# dictionary as the exact source.
Permission vocabulary (selected)
| Permission | Typical holders | Guards |
|---|---|---|
system:manage | PLATFORM_OPERATOR | ops regulatory admin, access-request review/provision, cross-org market ingest |
tenant:manage, tenant:view_audit | INSTITUTION_ADMIN, PLATFORM_OPERATOR | members, org settings/entitlements, audit view |
dossier:create / read / write / archive | admin, arranger, banker, analysts per role | dossier lifecycle; archive needs rationale ≥ 5 chars |
financial:read / write / reconcile / override | analysts, arranger, risk, portfolio | market overview/ingest, portfolio, reconciliation, cell override (financial:override, justification ≥ 5 chars) |
covenant:read / edit | arranger, analyst, risk, legal | covenant evaluation |
regulatory:read / audit | most roles; audit for arranger, compliance, legal | legal-vault resolve, open-statute, pre-visa audit |
memo:generate, memo:signoff | generate: most; signoff: ARRANGER_LEAD | memo drafter |
dealroom:upload / read_approved / manage_watermark | upload: admin, arranger, banker | document upload; approved-only reads |
qa:read / submit | most; guests submit sanitized questions | Q&A governance |
committee_notes:view / write | not SYNDICATE_GUEST | committee deliberations (Chinese wall) |
Enforcement layers
| Layer | Component | Failure |
|---|---|---|
| Edge | Middleware ticket check | redirect /login or 401 |
| BFF | requireBffSession + enforceRbacGuard (src/domains/auth/guard.ts, src/lib/api/locus-bff.ts) | 401 UNAUTHENTICATED / 401 SESSION_EXPIRED / 403 |
| SoR | InstitutionalAuthMiddleware → claims; InstitutionalAuthorization.RequirePermission | 403 PERMISSION_DENIED, 403 TENANT_REQUIRED |
| Data | EF query filters + FORCE RLS | zero rows / not found |
Cross-tenant dossier access is blocked with 403 TENANT_FORBIDDEN or not-found under the caller's OrganizationId. The matrix applies to both BFF /api/locus/* and every SoR controller (Deals, Documents, Members, Organizations, Exports, Market, Portfolio, Search, Alerts, Precedents, Blockers, AnalysisJobs, AccessRequests, Q&A, RegulatoryFindings, LegalVault). PLATFORM_OPERATOR additionally uses RegulatoryAdminController.
Verification suites: rbac_and_tenancy, auth_gate_and_api_rbac, dossier_persistence_rbac, market_portfolio_search_rbac, storage_health_and_r2.
3. Cookies and sessions: bank vs ops
| Cookie | App | Role allowed | Signing | Session guard |
|---|---|---|---|---|
locus_session | locus-web (bank, :3000) | any tenant role | SESSION_SIGNING_KEY (HMAC ticket, v1. prefix, expiry) | requireBffSession |
locus_ops_session | locus-ops (:3001) | PLATFORM_OPERATOR only | SESSION_SIGNING_KEY — the same key is recommended, but cookies are never shared | requireOpsSession |
Boundaries to remember:
- The two cookies are independent: a bank ticket does not authenticate ops, and an ops ticket does not authenticate
/workspace. - Both tickets are only the edge proof (signature + expiry). Real authority is the Postgres
AuthSessionsrow; logout revokes it server-side, so a stolen but revoked ticket stops working at the BFF/SoR. - Bank login: credentials → mandatory OTP. Ops login:
POST /api/auth/platform-loginrejects non-PLATFORM_OPERATORaccounts before the OTP step. - Bank BFF forwards
Authorization: Bearer <sessionId>andX-Organization-Idderived from the session.X-Organization-Id/X-User-*from a browser are never trusted in production (Auth__AllowDevHeaders=false). - Public bank routes (no cookie): landing,
/login,/register,/request-access*,/auth/accept-invite,/legal/*, and part of/api/auth. Everything else under/workspace/**redirects to/login; protected/api/**returns401JSON. - Legacy bank paths
/workspace/regulatory-adminand/workspace/access-requestsredirect to ops viaNEXT_PUBLIC_OPS_APP_URL.
Session lifetimes on the SoR: login 8 h, accept-invite 72 h. See Auth & sessions.
4. Identity chain
- Edge middleware verifies the signed ticket (presence + signature + expiry).
- BFF
requireBffSession(bank) /requireOpsSession(ops) loads the SoR session and rejects the wrong role for the app. - C#
InstitutionalAuthMiddlewarevalidatesAuthSessions(TokenHash = SHA-256(sessionId)), loads the Active membership, and sets claimssub,tenant_id,role,session_id. HttpContextTenantProviderprefers claims over client headers.TenantRlsMiddlewaresetsapp.current_tenant_id/app.is_platform_operatorGUCs;TenantRlsConnectionInterceptorapplies them per connection for FORCE RLS.- Controllers call
InstitutionalAuthorization.RequirePermission(...).
5. Other trust edges
| Edge | Rule |
|---|---|
| Browser → Next | Next never opens Postgres or spawns OS subprocesses; product writes go through the SoR |
| Next → SoR | Bearer session; org and role from the session, not from request headers |
| SoR → Workers | Internal HTTP (WorkerSettings__PythonWorkerUrl); typed AnalysisResult JSON; no LLM for numbers |
| Next → Workers (OCR) | LOCUS_WORKER_URL, POST /api/v1/ocr when native/harvest extraction is insufficient |
| Web/Ops → R2 | Private bucket; tenant PDFs encrypted at rest; vault PDFs hash-verified on read |
| Copilot → LLM | Optional prose polish only (COPILOT_LLM_PROVIDER, default off); numbers are untouchable; regulatory_corpus scope blocks dossier financial computation |
| Watchtower → vault | Discovery only; human APPROVE is the sole path to in_force |
Related: Security · Data stores · Ops overview · locus-backend.