from __future__ import annotations import os import subprocess import time from collections.abc import Callable from qonto_assistant.config import Settings from qonto_assistant.contracts import QontoCredentials from qonto_assistant.errors import CredentialError class EnvironmentCredentialProvider: def get_credentials(self) -> QontoCredentials: api_key = os.getenv("API_KEY") or os.getenv("QONTO_API_KEY") api_user = os.getenv("API_USER") or os.getenv("QONTO_ORGANIZATION_ID") if not api_key or not api_user: raise CredentialError( "Missing Qonto credentials in environment (API_KEY/API_USER or QONTO_* vars)" ) return QontoCredentials(api_user=api_user, api_key=api_key) def invalidate(self) -> None: return None class OpenBaoCliCredentialProvider: def __init__( self, *, command: str, path: str, timeout_seconds: float, ttl_seconds: int, runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run, ) -> None: self.command = command self.path = path self.timeout_seconds = timeout_seconds self.ttl_seconds = ttl_seconds self.runner = runner self._cached: tuple[float, QontoCredentials] | None = None def get_credentials(self) -> QontoCredentials: now = time.monotonic() if self._cached is not None and now - self._cached[0] < self.ttl_seconds: return self._cached[1] api_key = self._read_field("API_KEY") api_user = self._read_field("API_USER") credentials = QontoCredentials(api_user=api_user, api_key=api_key) self._cached = (now, credentials) return credentials def invalidate(self) -> None: self._cached = None def _read_field(self, field_name: str) -> str: result = self.runner( [self.command, "kv", "get", f"-field={field_name}", self.path], check=False, capture_output=True, text=True, timeout=self.timeout_seconds, ) if result.returncode != 0: raise CredentialError( f"OpenBao CLI failed while reading {field_name} from {self.path}: " f"{result.stderr.strip() or result.stdout.strip() or 'unknown error'}" ) value = result.stdout.strip() if not value: raise CredentialError(f"OpenBao returned an empty value for {field_name}") return value def build_credential_provider( settings: Settings, ) -> EnvironmentCredentialProvider | OpenBaoCliCredentialProvider: if settings.credential_source == "bao-cli": return OpenBaoCliCredentialProvider( command=settings.openbao_command, path=settings.openbao_path, timeout_seconds=settings.openbao_timeout_seconds, ttl_seconds=settings.qonto_secret_ttl_seconds, ) if settings.credential_source == "env": return EnvironmentCredentialProvider() raise CredentialError(f"Unsupported credential source: {settings.credential_source}")