b135f11557
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>
76 lines
2.5 KiB
Python
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()
|