Implements AUDIT_REPORT.md items H-1, H-2, M-1, M-4, M-6, L-3:
- H-1: enable FluentValidation auto-validation — the registered validators (incl.
Confirmed-required-for-destructive) now actually execute; invalid DTOs 400 at the
boundary instead of reaching services.
- H-2: ASP.NET Core rate limiting — global per-user/per-IP fixed window (300/min
default) + stricter 'auth' (10/min) and 'expensive' (20/min: export, unsubscribe,
AI) policies; config-driven; 429 with no queue.
- M-1: absolute session lifetime (30d default) — an issued-at stamp set at sign-in
and checked in OnValidatePrincipal, so a stolen cookie can no longer slide-renew
forever. Pre-existing sessions re-login once.
- M-4: remove the guessable default DB password from appsettings; startup fails fast
with a clear message when the connection string has no password (compose/staging
inject the real one).
- M-6: matching tenant query filter on EmailLabel (via Email navigation) — clears
the long-standing EF boot warning and closes the join-row leak window.
- L-3: SMTP skip-notice logging downgraded to Debug (recipient address is PII-ish).
Tests: 6 new (400-on-invalid x2, 429 auth rate limit via a test auth scheme,
session-lifetime x3, EmailLabel cross-user invisibility). Full suite: 48/48 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 5 re-validation caught a functional regression: the V-10 clamp in
SearchService.SearchAsync also capped CleanupService's internal target resolution
(pageSize 10000 -> 200), silently limiting bulk cleanup-by-query to 200 emails.
The clamp belongs at the user-facing trust boundary, not the shared service: move
MaxPageSize (200) enforcement into SearchController (both the POST body and GET query
paths). Internal callers of ISearchService now request large pages unhindered, while
user requests are still bounded. Adds a regression test proving SearchService returns
a 250-row page uncapped. No security regressions per Phase 5. All 39 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Defense-in-depth tenant isolation: every user-owned entity (Email, Sender,
MailThread, MailDomain, Attachment, Label, SyncState, AnalyticsAggregate,
WidgetLayout, UnsubscribeItem) gets a global query filter restricting reads to the
authenticated user. AppDbContext takes an optional ICurrentUser; CurrentUserId is
Guid.Empty for background workers / design-time, which DISABLES the filter so sync
and tooling (which already scope by an explicit userId) are unaffected. On the HTTP
attack surface a forgotten manual `WHERE UserId ==` can no longer leak another
tenant's rows.
Phase 1 confirmed no active IDOR; this is preventive, and prioritised now because the
upcoming automation engine will add many new queries.
Also: moved the Npgsql-only tsvector FTS mapping out of EmailConfiguration into
AppDbContext.OnModelCreating, guarded by Database.IsRelational() (Ignored otherwise),
so non-relational test providers work — honouring the existing Email.SearchVector
comment. Production (Npgsql) model is unchanged; no migration needed.
Adds 3 cross-user tenant-isolation integration tests. All 38 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>