client/securedrop_client/config.py (54 lines of code) (raw):
import logging
import os
from collections.abc import Generator
from contextlib import contextmanager
from dataclasses import MISSING, dataclass, fields
logger = logging.getLogger(__name__)
@contextmanager
def try_qubesdb() -> Generator:
"""Minimal context manager around QubesDB() → QubesDB.close() when
available."""
db: bool | "QubesDB" = False # noqa: UP037
try:
from qubesdb import QubesDB
db = QubesDB()
yield db
except ImportError:
logger.debug("QubesDB not available")
yield db
finally:
if db:
db.close() # type: ignore[union-attr]
@dataclass
class Config:
"""Configuration loaded at runtime from QubesDB (if available) or
environment variables."""
# Mapping of `Config` attributes (keys) to how to look them up (values)
# from either QubesDB or the environment.
mapping = {
"gpg_domain": "QUBES_GPG_DOMAIN",
"journalist_key_fingerprint": "SD_SUBMISSION_KEY_FPR",
"download_retry_limit": "SD_DOWNLOAD_RETRY_LIMIT",
}
journalist_key_fingerprint: str
gpg_domain: str | None = None
download_retry_limit: int = 3
@classmethod
def load(cls) -> "Config":
"""For each attribute, look it up from either QubesDB or the environment."""
config = {}
with try_qubesdb() as db:
for field in fields(cls):
lookup = cls.mapping[field.name]
if db:
logger.debug(f"Reading {lookup} from QubesDB")
value = db.read(f"/vm-config/{lookup}")
if not value or len(value) == 0:
if field.default == MISSING:
raise KeyError(f"Could not read {lookup} from QubesDB")
# Normalize for parity with the case where os.environ.get() is None
value = None
else:
logger.debug(f"Reading {lookup} from environment")
value = os.environ.get(lookup)
if not value or len(value) == 0:
# Same normalization used for QubesDB
value = None
if value is None and field.default != MISSING:
logger.debug(f"Using default value for {lookup}")
value = field.default
# Cast to int if needed (might raise if value is invalid)
# TODO: in theory we could `field.type(value)` but that doesn't
# handle union types
if field.type is int:
value = int(value)
config[field.name] = value
return cls(**config)