from __future__ import annotations import os import secrets from dataclasses import dataclass from pathlib import Path def load_dotenv(path: str = ".env") -> None: env_path = Path(path) if not env_path.exists(): return for raw_line in env_path.read_text().splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) def _bool(name: str, default: bool = False) -> bool: value = os.environ.get(name) if value is None: return default return value.lower() in {"1", "true", "yes", "on"} def _csv(name: str) -> set[str]: return { item.strip().lower() for item in os.environ.get(name, "").split(",") if item.strip() } #: A "home" deployment serves the public landing page and nothing else: no database, no session, #: no OIDC. Everything that needs an identity lives on the "full" deployment at auth_base_url. ROLES = {"full", "home"} @dataclass(frozen=True) class Config: role: str auth_base_url: str base_url: str host: str port: int secret: str cookie_secure: bool database_url: str trusted_domains: set[str] default_ipa_group: str app_admin_emails: set[str] admin_groups: set[str] oidc_issuer: str oidc_client_id: str oidc_client_secret: str oidc_scopes: str smtp_host: str smtp_port: int smtp_username: str smtp_password: str smtp_from: str smtp_starttls: bool smtp_ssl: bool ipa_mode: str ipa_principal: str ipa_keytab: str ipa_default_shell: str ipa_home_root: str authentik_sync_enabled: bool authentik_sync_container: str authentik_ldap_source_slugs: tuple[str, ...] def get_config() -> Config: load_dotenv() role = os.environ.get("APP_ROLE", "full").strip().lower() if role not in ROLES: raise SystemExit(f"APP_ROLE must be one of {sorted(ROLES)}, got {role!r}") base_url = os.environ.get("APP_BASE_URL", "http://127.0.0.1:8080").rstrip("/") database_url = os.environ.get("DATABASE_URL", "") if role != "home" and not database_url: raise SystemExit("DATABASE_URL is required unless APP_ROLE=home") # Where the pages this deployment does not serve actually live -- the "full" deployment that # owns registration, profile and admin. A "full" deployment serves them itself, so its own # base URL is the right answer. This is NOT the identity provider: OIDC_ISSUER points at # Authentik, which lives on a different host. auth_base_url = os.environ.get("AUTH_BASE_URL", base_url).rstrip("/") oidc_issuer = os.environ.get("OIDC_ISSUER", "").rstrip("/") if role != "home" and not oidc_issuer: raise SystemExit("OIDC_ISSUER is required unless APP_ROLE=home") return Config( role=role, auth_base_url=auth_base_url, base_url=base_url, host=os.environ.get("APP_HOST", "127.0.0.1"), port=int(os.environ.get("APP_PORT", "8080")), secret=os.environ.get("APP_SECRET", secrets.token_hex(32)), cookie_secure=_bool("COOKIE_SECURE", False), database_url=database_url, trusted_domains=_csv("TRUSTED_EMAIL_DOMAINS"), default_ipa_group=os.environ.get("DEFAULT_IPA_GROUP", "sticknife_users"), app_admin_emails=_csv("APP_ADMIN_EMAILS"), admin_groups=_csv("APP_ADMIN_GROUPS") or {"sticknife_admins"}, oidc_issuer=oidc_issuer, oidc_client_id=os.environ.get("OIDC_CLIENT_ID", ""), oidc_client_secret=os.environ.get("OIDC_CLIENT_SECRET", ""), oidc_scopes=os.environ.get("OIDC_SCOPES", "openid profile email groups"), smtp_host=os.environ.get("SMTP_HOST", "mail.sticknife.com"), smtp_port=int(os.environ.get("SMTP_PORT", "587")), smtp_username=os.environ.get("SMTP_USERNAME", ""), smtp_password=os.environ.get("SMTP_PASSWORD", ""), smtp_from=os.environ.get("SMTP_FROM", "accounts@sticknife.com"), smtp_starttls=_bool("SMTP_STARTTLS", True), smtp_ssl=_bool("SMTP_SSL", False), ipa_mode=os.environ.get("STICKNIFE_IPA_PROVISION_MODE", "dry-run"), ipa_principal=os.environ.get("IPA_PRINCIPAL", ""), ipa_keytab=os.environ.get("IPA_KEYTAB", ""), ipa_default_shell=os.environ.get("IPA_DEFAULT_SHELL", "/bin/bash"), ipa_home_root=os.environ.get("IPA_HOME_ROOT", "/home"), authentik_sync_enabled=_bool("AUTHENTIK_SYNC_ENABLED", False), authentik_sync_container=os.environ.get("AUTHENTIK_SYNC_CONTAINER", "authentik-worker-1"), authentik_ldap_source_slugs=tuple(sorted(_csv("AUTHENTIK_LDAP_SOURCE_SLUGS"))), )