Files
alembic/app/main.py
T
andrew b135f11557 Security hardening and first-run/portability improvements
Security (P0):
- Remove committed session-secret default; auto-generate and persist a
  random secret to the config volume when SESSION_SECRET is unset
  (prevents forgeable session cookies / auth bypass).
- Validate playlist names to a safe charset and render sldl configs via
  literal Python substitution instead of sed (closes a command-injection
  and path-traversal path through playlist names).
- shlex-quote credential values written to shell-sourced env files, and
  strip newlines from values patched into .conf files.
- Render playlist .conf files 0600; warn at startup if the master key is
  co-located with the config volume; document keeping it separate.

Portability:
- Configurable timezone via TZ (default UTC) instead of hardcoded Edmonton.
- Remove personal defaults (navidrome user "andrew", ephemeral.club URLs).
- Ship generic example seeds; move the cross-album dedup keep-list and the
  legacy playlist import to editable config files; drop the personal
  _upgrade.csv.
- Generic VPN reference in docker-compose.snippet.yml.

First-run experience:
- Redirect to /setup instead of 500 when OIDC is unconfigured; surface a
  missing master key inline; entrypoint exits with an actionable message
  when the config folder is not writable.
- Add unauthenticated /health (JSON) and /setup (checklist) diagnostics.

Docs:
- Write docs/ARCHITECTURE.md and docs/MIGRATION.md (previously referenced
  but missing); expand README with ownership, backups, advanced settings,
  and migration guidance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 14:25:55 -06:00

76 lines
2.5 KiB
Python

import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from app.db import enable_beets_db_wal, init_db
from app.routers import artist_casing, auth, credentials, dashboard, dedup, genres, health, import_, jobs, library, playlists
from app.security.session import resolve_session_secret
from app.services import scheduler_service
from app.settings import settings
log = logging.getLogger("alembic")
def _warn_if_key_colocated_with_config() -> None:
"""The Fernet master key decrypts every stored credential. If it lives
inside the config volume, one leaked volume or backup exposes both the
key and the encrypted database together, which defeats the encryption.
Warn loudly rather than fail, since some users deliberately accept this."""
try:
key_path = settings.encryption_key_file.resolve()
config_dir = settings.alembic_config_dir.resolve()
except OSError:
return
if config_dir == key_path or config_dir in key_path.parents:
log.warning(
"Master key %s is inside the config directory %s. Anyone with a "
"copy of that volume or backup gets both the key and the encrypted "
"credentials. Mount the key from a separate location kept out of "
"the config backup. See docker-compose.snippet.yml.",
key_path,
config_dir,
)
@asynccontextmanager
async def lifespan(_app: FastAPI):
_warn_if_key_colocated_with_config()
init_db()
enable_beets_db_wal()
scheduler = scheduler_service.create_scheduler()
scheduler_service.register_all_jobs(scheduler)
scheduler.start()
yield
scheduler.shutdown(wait=False)
def create_app() -> FastAPI:
app = FastAPI(title="alembic", lifespan=lifespan)
app.add_middleware(SessionMiddleware, secret_key=resolve_session_secret())
app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.include_router(health.router)
app.include_router(auth.router)
app.include_router(dashboard.router)
app.include_router(library.router)
app.include_router(import_.router)
app.include_router(dedup.router)
app.include_router(genres.router)
app.include_router(playlists.router)
app.include_router(credentials.router)
app.include_router(jobs.router)
app.include_router(artist_casing.router)
return app
app = create_app()