# 04 — Security

Security is a launch requirement, not a hardening pass. Every module spec
inherits this document; module-specific rules add to it, never relax it.

## 1. Authentication

- Staff app: Laravel session auth, `bcrypt`/`argon2id` hashing, enforced
  password policy (min 12 chars, breach-check via `Have I Been Pwned` k-anonymity
  API — `laravel/fortify`-style check), account lockout after 5 failed attempts
  (`RateLimiter`, per-email + per-IP).
- **Mandatory TOTP 2FA** for any role with financial approval or admin permission;
  optional-but-encouraged for everyone else. Recovery codes generated, stored
  hashed.
- Client portal: separate guard (`portal`), separate session, separate password
  policy; no cross-guard privilege ever possible (a portal session can NEVER
  resolve a staff `User`).
- Session config: `secure`, `http_only`, `same_site=lax` cookies; idle timeout 30
  min for staff, 20 min for portal; regenerate session ID on login/logout/2FA.

## 2. Authorization model

- **RBAC via Spatie**: Roles (Managing Partner, Partner, Counsel, Paralegal,
  Accounts, Firm Admin, Client) map to Permissions (`matters.view`,
  `matters.create`, `finance.approve`, …). Full matrix maintained in
  `docs/modules/M01-foundation.md`.
- **Row-level via Policies**: every model with ownership semantics gets a
  Policy (`MatterPolicy`, `InvoicePolicy`, `DocumentPolicy`…). Two-layer check on
  every controller action: `$this->authorize('view', $matter)` — permission
  AND policy both pass, never permission alone for firm-sensitive data.
- **Assigned-only visibility**: a Counsel without `matters.viewAll` sees only
  matters where they're on `matter_team`. Implemented once as
  `Matter::visibleTo($user)` scope, reused everywhere (list, dashboard, search,
  reports) — never re-implemented ad hoc.
- **Firm isolation**: `FirmScope` global scope on every tenant model; explicitly
  tested per module (`assertCrossFirmDenied`) so a regression fails CI, not
  production.

## 3. OWASP Top 10 — how each is addressed

| Risk | Mitigation |
|---|---|
| Broken Access Control | Policies + permission middleware on every route; automated route audit test asserting no route lacks a permission/policy check |
| Cryptographic Failures | TLS-only in production (HSTS), `argon2id` hashing, `APP_KEY`-encrypted sensitive columns (bank details, portal invite tokens) via Laravel encrypted casts |
| Injection | Eloquent/query builder only — no raw SQL string interpolation; FormRequests validate/sanitize all input; CSP headers restrict inline scripts |
| Insecure Design | Threat-modeled per module (trust accounting overdraw, portal IDOR, document confidentiality) — see module specs' "security notes" |
| Security Misconfiguration | `APP_DEBUG=false` in prod enforced by deploy checklist; security headers middleware (CSP, X-Frame-Options DENY, X-Content-Type-Options, Referrer-Policy); `.env` never committed |
| Vulnerable Components | `composer audit` + Dependabot/Renovate in CI; pinned versions, no `dev-master` deps |
| Auth Failures | 2FA, lockout, session rules above; password reset tokens single-use, 60-min expiry |
| Software & Data Integrity | Signed queue jobs where relevant; document versions immutable (append-only) with checksum (`sha256`) stored per version |
| Logging & Monitoring Failures | `activitylog` on every mutation; separate audit log channel; failed authorization attempts logged; alerting hook (Sentry) for repeated auth failures |
| SSRF | No user-supplied URLs fetched server-side; webhook/URL fields (future integrations) go through an allow-list resolver |

## 4. Data protection & privacy (NDPR-aligned)

Nigeria Data Protection Act/NDPR treats client and staff personal data as
regulated. Baseline posture:
- Data minimization: only collect fields the module spec justifies.
- Purpose limitation: client data used for matter service, billing, and portal
  access only — no secondary use without consent flag.
- Right to access/export: a client (via staff) can receive a data export of
  their held information (contacts, matters summary, invoices) — built as an
  admin action in M02.
- Sensitive fields (NIN, bank account numbers, date of birth) stored with
  Laravel's `encrypted` cast; masked in the UI by default (show-on-click, itself
  audit-logged).
- Data breach runbook documented in `docs/06-DEVELOPMENT-PLAN.md` operational
  appendix.

## 5. Confidentiality tiers (documents & matters)

Three tiers, enforced by Policy, not just UI hiding:
1. **Standard** — visible to matter team + firm-wide `documents.viewAll`.
2. **Confidential** — matter team only, requires `documents.viewConfidential`.
3. **Privileged/Ethical Wall** — explicit allow-list of users regardless of
   team membership (for conflict-sensitive matters); managed by Firm Admin only.

## 6. Client portal security (highest-risk surface)

- Every portal query scoped to `auth('portal')->user()->client_id` — centralized
  in a `PortalScoped` trait/base controller, never trusted from a route
  parameter.
- IDOR test suite: for every portal route, assert client A cannot reach client
  B's record by guessing an ID/ULID.
- Document sharing is opt-in per document (explicit staff action); confidential/
  privileged documents cannot be shared — hard-blocked in the sharing Action,
  not just hidden in the UI.
- No portal endpoint ever serializes internal remarks, comments, other parties'
  contact details, or non-client-facing financials — enforced via dedicated
  Portal Resource/DTO classes distinct from staff-side serialization.
- Rate limiting + CAPTCHA-free honeypot on portal forms; portal never gets write
  access to matters, only to requests/messages that staff must approve/action.

## 7. Financial integrity controls

- All postings via `LedgerService`; direct writes to `journal_lines` outside it
  are impossible (no public setter path — the service is the only caller in
  code review terms).
- Trust/retainer drawdown cannot exceed balance — enforced in the Action with a
  DB-level check (row lock + assertion) to survive concurrent requests.
- Segregation of duties: the user who creates an appearance-fee claim cannot
  approve it (`approve.own` explicitly denied in the Policy, tested).
- Every financial document (invoice, receipt, journal) is immutable once
  posted; corrections are always a new counter-entry, never an edit.

## 8. Audit & monitoring

- `activitylog` on all create/update/delete for domain models, with
  causer + before/after diff.
- Dedicated **security events** log (login success/fail, permission denials,
  2FA changes, portal access grants, document confidentiality changes,
  data exports) — reviewable by Firm Admin in-app (M12).
- Weekly automated report: failed logins, permission-denied spikes, overdue
  backups — emailed to Firm Admin.

## 9. Backups & recovery

- Nightly encrypted DB backup + weekly file backup (`spatie/laravel-backup`) to
  off-site storage (S3-compatible); restore drill documented and rehearsed
  before go-live (see Development Plan Phase 8).
- Backup integrity check job verifies the latest backup is restorable monthly.

## 10. Security checklist gate (must pass before go-live)

- [ ] Every route has permission middleware; automated route-audit test green
- [ ] Every sensitive model has a Policy; policy test suite green
- [ ] Cross-firm isolation tests green across all modules
- [ ] Portal IDOR test suite green
- [ ] 2FA enforced for privileged roles
- [ ] Security headers present on all responses (verified via test)
- [ ] `composer audit` clean; no dev dependencies in production build
- [ ] Backup restore drill completed successfully
- [ ] `.env` and secrets confirmed absent from version control history
