Pre-release audit fixes: - External Administrators can only act on investors already in their own funds (grant_access, create_statement, set_partner_exited, import match), closing a cross-fund account-takeover chain. - Role and is_active changes are gated: no self-escalation, non-approvers cannot mint or alter approver accounts, and the Service Admin is protected. - must_change_password is enforced server-side, not just in the client. - Session cookie is Secure with a 12h max-age and SameSite; CSP and standard security headers on every response; the app refuses to boot on a known or short session secret; uvicorn runs with proxy-headers so login throttling keys on the real client IP behind the platform proxy. - Import preview no longer confirms cross-fund investors; no-store on document downloads. - Adds backend/tests/test_security_hardening.py (15 tests); full suite 76 passing. - SECURITY-AUDIT.md records the swarm audit, findings, and remediation status.
37 lines
2.2 KiB
Python
37 lines
2.2 KiB
Python
"""Application configuration via environment variables."""
|
|
|
|
import os
|
|
|
|
|
|
DB_PATH: str = os.getenv("TEN31_DB_PATH", "/data/ten31portal/portal.db")
|
|
SESSION_SECRET: str = os.getenv("TEN31_SESSION_SECRET", "change-me-in-production")
|
|
# Session secrets that must never reach production. The app refuses to start on any of these
|
|
# (see main.lifespan) so a packaging slip cannot silently boot on a source-visible key that
|
|
# would let anyone forge a session cookie.
|
|
INSECURE_SESSION_SECRETS: frozenset[str] = frozenset({
|
|
"", "change-me", "change-me-in-production",
|
|
})
|
|
# Send the session cookie only over HTTPS. Off by default so local dev and the test client
|
|
# (both plain HTTP) work; the container turns it on via TEN31_SESSION_COOKIE_SECURE=true.
|
|
SESSION_COOKIE_SECURE: bool = os.getenv(
|
|
"TEN31_SESSION_COOKIE_SECURE", "false"
|
|
).lower() in ("1", "true", "yes")
|
|
# Session lifetime. Default 12 hours (was the Starlette 14-day default), so a captured cookie
|
|
# ages out in a workday rather than a fortnight.
|
|
SESSION_MAX_AGE: int = int(os.getenv("TEN31_SESSION_MAX_AGE", str(12 * 60 * 60)))
|
|
DOCS_DIR: str = os.getenv("TEN31_DOCS_DIR", "/data/ten31portal/documents")
|
|
# Cap on a single uploaded document. The docs dir shares the data volume with the DB, so an
|
|
# unbounded upload could fill the disk and take the portal down. Default 50 MB. The same cap
|
|
# also bounds spreadsheet imports (which must be read fully into memory to parse).
|
|
MAX_UPLOAD_SIZE: int = int(os.getenv("TEN31_MAX_UPLOAD_SIZE", str(50 * 1024 * 1024)))
|
|
|
|
# Investor accounts created by the eNAV import (and existing no-login accounts converted via
|
|
# the enable-investor-logins CLI/action) start with this password so the admin can hand out
|
|
# credentials easily; each investor changes it via the portal's own Change password.
|
|
DEFAULT_INVESTOR_PASSWORD: str = os.getenv("TEN31_DEFAULT_INVESTOR_PASSWORD", "Ten31Portal")
|
|
|
|
# Where start.sh records the randomly-generated initial admin password on first boot, so the
|
|
# operator can retrieve it once (via the "Show Initial Admin Password" service action) and then
|
|
# change it. Lives next to the DB on the 0600 data volume; removed once the password is reset.
|
|
ADMIN_PASSWORD_FILE: str = os.path.join(os.path.dirname(DB_PATH) or ".", ".admin-password")
|