Security:
- DEBT-008: remove query-string token auth; header-only Bearer now enforced
- DEBT-013: add regex constraint ^[a-z0-9\-]{1,64}$ on decky_name path param
- DEBT-015: stop leaking raw exception detail to API clients; log server-side
- DEBT-016: validate search (max_length=512) and datetime params with regex
Reliability:
- DEBT-014: wrap SSE event_generator in try/except; yield error frame on failure
- DEBT-017: emit log.warning/error on DB init retry; silent failures now visible
Observability / Docs:
- DEBT-020: add 401/422 response declarations to all route decorators
Infrastructure:
- DEBT-018: add HEALTHCHECK to all 24 template Dockerfiles
- DEBT-019: add USER decnet + setcap cap_net_bind_service to all 24 Dockerfiles
- DEBT-024: bump Redis template version 7.0.12 → 7.2.7
Config:
- DEBT-012: validate DECNET_API_PORT and DECNET_WEB_PORT range (1-65535)
Code quality:
- DEBT-010: delete 22 duplicate decnet_logging.py copies; deployer injects canonical
- DEBT-022: closed as false positive (print only in module docstring)
- DEBT-009: closed as false positive (templates already use structured syslog_line)
Build:
- DEBT-025: generate requirements.lock via pip freeze
Testing:
- DEBT-005/006/007: comprehensive test suite added across tests/api/
- conftest: in-memory SQLite + StaticPool + monkeypatched session_factory
- fuzz mark added; default run excludes fuzz; -n logical parallelism
DEBT.md updated: 23/25 items closed; DEBT-011 (Alembic) and DEBT-023 (digest pinning) remain
62 lines
2.4 KiB
Python
62 lines
2.4 KiB
Python
import os
|
||
from pathlib import Path
|
||
from dotenv import load_dotenv
|
||
|
||
# Calculate absolute path to the project root
|
||
_ROOT: Path = Path(__file__).parent.parent.absolute()
|
||
|
||
# Load .env.local first, then fallback to .env
|
||
load_dotenv(_ROOT / ".env.local")
|
||
load_dotenv(_ROOT / ".env")
|
||
|
||
|
||
def _port(name: str, default: int) -> int:
|
||
raw = os.environ.get(name, str(default))
|
||
try:
|
||
value = int(raw)
|
||
except ValueError:
|
||
raise ValueError(f"Environment variable '{name}' must be an integer, got '{raw}'.")
|
||
if not (1 <= value <= 65535):
|
||
raise ValueError(f"Environment variable '{name}' must be 1–65535, got {value}.")
|
||
return value
|
||
|
||
|
||
def _require_env(name: str) -> str:
|
||
"""Return the env var value or raise at startup if it is unset or a known-bad default."""
|
||
_KNOWN_BAD = {"fallback-secret-key-change-me", "admin", "secret", "password", "changeme"}
|
||
value = os.environ.get(name)
|
||
if not value:
|
||
raise ValueError(
|
||
f"Required environment variable '{name}' is not set. "
|
||
f"Set it in .env.local or export it before starting DECNET."
|
||
)
|
||
|
||
if any(k.startswith("PYTEST") for k in os.environ):
|
||
return value
|
||
|
||
if value.lower() in _KNOWN_BAD:
|
||
raise ValueError(
|
||
f"Environment variable '{name}' is set to an insecure default ('{value}'). "
|
||
f"Choose a strong, unique value before starting DECNET."
|
||
)
|
||
return value
|
||
|
||
|
||
# API Options
|
||
DECNET_API_HOST: str = os.environ.get("DECNET_API_HOST", "0.0.0.0") # nosec B104
|
||
DECNET_API_PORT: int = _port("DECNET_API_PORT", 8000)
|
||
DECNET_JWT_SECRET: str = _require_env("DECNET_JWT_SECRET")
|
||
DECNET_INGEST_LOG_FILE: str | None = os.environ.get("DECNET_INGEST_LOG_FILE", "/var/log/decnet/decnet.log")
|
||
|
||
# Web Dashboard Options
|
||
DECNET_WEB_HOST: str = os.environ.get("DECNET_WEB_HOST", "0.0.0.0") # nosec B104
|
||
DECNET_WEB_PORT: int = _port("DECNET_WEB_PORT", 8080)
|
||
DECNET_ADMIN_USER: str = os.environ.get("DECNET_ADMIN_USER", "admin")
|
||
DECNET_ADMIN_PASSWORD: str = os.environ.get("DECNET_ADMIN_PASSWORD", "admin")
|
||
DECNET_DEVELOPER: bool = os.environ.get("DECNET_DEVELOPER", "False").lower() == "true"
|
||
|
||
# CORS — comma-separated list of allowed origins for the web dashboard API.
|
||
# Example: DECNET_CORS_ORIGINS=http://localhost:8080,https://dashboard.example.com
|
||
_cors_raw: str = os.environ.get("DECNET_CORS_ORIGINS", "http://localhost:8080")
|
||
DECNET_CORS_ORIGINS: list[str] = [o.strip() for o in _cors_raw.split(",") if o.strip()]
|