65 lines
2.8 KiB
Python
65 lines
2.8 KiB
Python
from dataclasses import dataclass, field, fields
|
|
import os
|
|
from pathlib import Path
|
|
import tomllib
|
|
from urllib.parse import urlsplit
|
|
|
|
|
|
@dataclass
|
|
class Settings:
|
|
data_dir: Path = Path("data")
|
|
master_key_file: Path = Path("secrets/master.key")
|
|
public_url: str = "https://localhost:8080"
|
|
secure_cookies: bool = True
|
|
bootstrap_username: str | None = None
|
|
bootstrap_password: str | None = None
|
|
testing: bool = False
|
|
session_hours: int = 8
|
|
answer_window_seconds: int = 300
|
|
enrollment_hours: int = 4
|
|
lease_seconds: int = 900
|
|
heartbeat_unknown_seconds: int = 180
|
|
log_retention_days: int = 30
|
|
audit_retention_days: int = 180
|
|
max_request_bytes: int = 1048576
|
|
four_eyes: bool = True
|
|
maintenance: bool = False
|
|
trusted_proxy_ips: str = "127.0.0.1"
|
|
runner_ca_file: str | None = None
|
|
defaults: dict = field(default_factory=dict)
|
|
sites: dict = field(default_factory=dict)
|
|
|
|
def __post_init__(self):
|
|
self.data_dir = Path(self.data_dir)
|
|
self.master_key_file = Path(self.master_key_file)
|
|
self.public_url = self.public_url.rstrip("/")
|
|
url = urlsplit(self.public_url)
|
|
if not url.hostname or url.username or url.password or url.query or url.fragment or url.path:
|
|
raise ValueError("PUBLIC_URL muss eine absolute Basis-URL ohne Pfad sein.")
|
|
if url.scheme != "https" and not (url.scheme == "http" and url.hostname in {"localhost", "127.0.0.1", "testserver"}):
|
|
raise ValueError("PUBLIC_URL benötigt HTTPS; HTTP ist nur für lokale Entwicklung erlaubt.")
|
|
if self.master_key_file.resolve().is_relative_to(self.data_dir.resolve()):
|
|
raise ValueError("Der Master-Key muss außerhalb des Datenverzeichnisses liegen.")
|
|
for name in ("session_hours", "answer_window_seconds", "enrollment_hours", "lease_seconds", "max_request_bytes"):
|
|
if getattr(self, name) <= 0:
|
|
raise ValueError(f"{name} muss positiv sein.")
|
|
|
|
@classmethod
|
|
def from_env(cls):
|
|
values = {}
|
|
if os.environ.get("APP_CONFIG"):
|
|
with open(os.environ["APP_CONFIG"], "rb") as stream:
|
|
loaded = tomllib.load(stream)
|
|
values.update(loaded.get("app", loaded))
|
|
bools = {"secure_cookies", "testing", "four_eyes", "maintenance"}
|
|
ints = {f.name for f in fields(cls) if f.type is int}
|
|
allowed = {f.name for f in fields(cls)}
|
|
unknown = set(values) - allowed
|
|
if unknown:
|
|
raise ValueError(f"Unbekannte Konfiguration: {', '.join(sorted(unknown))}")
|
|
for key in allowed:
|
|
value = os.environ.get(key.upper())
|
|
if value is not None:
|
|
values[key] = value.lower() in {"true", "1", "yes"} if key in bools else int(value) if key in ints else value
|
|
return cls(**values)
|