Fifth commit in the session-expiration-policy series. Surfaces the
session-policy override controls to account owners.
- schemas/account_security.py: NEW. SessionPolicyResponse returns both
the override (Optional[int]) and the effective value (always present)
plus the system min/max bounds, so the frontend can render the
Custom-preset form without re-implementing the defaults logic.
SessionPolicyUpdateRequest accepts NULL to clear an override.
- endpoints/account_security.py: NEW. GET and PATCH on /me/security.
Owner-only via require_account_owner. PATCH validates per-field
bounds, then validates the effective idle <= absolute invariant
(catching the partial-override case the DB CHECK can't see), then
writes the row + an account.session_policy_update audit event with
old/new/effective_old/effective_new payload.
- router.py: registers the new router under _tenant_deps next to
accounts.router.
Tests added in test_session_policy.py (8 cases):
- GET returns NULL overrides + Strict defaults + system bounds.
- PATCH persists override; next login JWT reflects new values
(60min/240min -> idle_max=3600, abs_max=14400 seconds).
- PATCH rejects idle < min (422).
- PATCH rejects absolute > max (422).
- PATCH rejects idle > absolute when both are set (422).
- PATCH rejects partial override that produces effective idle >
effective absolute (idle=43200, absolute=NULL with default 20160).
- Engineer-role user gets 403.
- PATCH writes exactly one audit row with the expected payload shape.
16/16 in test_session_policy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>