provider.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. # -*- coding: utf-8 -*-
  2. """
  3. Abstrakte Basisklasse für Secrets-Provider.
  4. Definiert die Schnittstelle für alle Secrets-Provider
  5. Implementierungen.
  6. """
  7. from __future__ import annotations
  8. from abc import ABC, abstractmethod
  9. from dataclasses import dataclass, field
  10. from datetime import datetime
  11. from typing import Any, Generic, TypeVar
  12. T = TypeVar("T")
  13. @dataclass
  14. class SecretValue(Generic[T]):
  15. """
  16. Wrapper für einen Secret-Wert.
  17. Bietet Metadaten und sichere Handhabung für Secrets.
  18. """
  19. key: str
  20. """Schlüssel/Name des Secrets."""
  21. value: T
  22. """Der eigentliche Secret-Wert."""
  23. source: str = ""
  24. """Quelle des Secrets (z.B. 'env', 'file')."""
  25. created_at: datetime = field(default_factory=datetime.now)
  26. """Zeitpunkt des Ladens."""
  27. expires_at: datetime | None = None
  28. """Optionaler Ablaufzeitpunkt."""
  29. metadata: dict[str, Any] = field(default_factory=dict)
  30. """Zusätzliche Metadaten."""
  31. @property
  32. def is_expired(self) -> bool:
  33. """Prüft, ob das Secret abgelaufen ist."""
  34. if self.expires_at is None:
  35. return False
  36. return datetime.now() > self.expires_at
  37. def __repr__(self) -> str:
  38. """Sichere Repräsentation ohne Wert."""
  39. return f"SecretValue(key={self.key!r}, source={self.source!r})"
  40. def __str__(self) -> str:
  41. """Sichere String-Darstellung."""
  42. return f"<Secret: {self.key}>"
  43. class SecretsProvider(ABC):
  44. """
  45. Abstrakte Basisklasse für Secrets-Provider.
  46. Definiert die Schnittstelle zum Laden, Speichern und
  47. Verwalten von Secrets.
  48. Implementierungen können verschiedene Backends nutzen:
  49. - Umgebungsvariablen
  50. - Verschlüsselte Dateien
  51. - Cloud Secret Manager (AWS, Azure, GCP)
  52. - HashiCorp Vault
  53. """
  54. def __init__(self, name: str = "") -> None:
  55. """
  56. Initialisiert den Provider.
  57. Args:
  58. name: Name des Providers für Logging.
  59. """
  60. self._name = name or self.__class__.__name__
  61. self._cache: dict[str, SecretValue] = {}
  62. self._cache_enabled = True
  63. @property
  64. def name(self) -> str:
  65. """Gibt den Provider-Namen zurück."""
  66. return self._name
  67. @property
  68. def cache_enabled(self) -> bool:
  69. """Ob Caching aktiviert ist."""
  70. return self._cache_enabled
  71. @cache_enabled.setter
  72. def cache_enabled(self, value: bool) -> None:
  73. """Aktiviert/Deaktiviert Caching."""
  74. self._cache_enabled = value
  75. if not value:
  76. self._cache.clear()
  77. @abstractmethod
  78. def get(self, key: str, default: T | None = None) -> T | None:
  79. """
  80. Holt einen Secret-Wert.
  81. Args:
  82. key: Schlüssel des Secrets.
  83. default: Standardwert falls nicht gefunden.
  84. Returns:
  85. Secret-Wert oder Default.
  86. """
  87. pass
  88. @abstractmethod
  89. def get_secret(self, key: str) -> SecretValue | None:
  90. """
  91. Holt ein Secret mit Metadaten.
  92. Args:
  93. key: Schlüssel des Secrets.
  94. Returns:
  95. SecretValue oder None.
  96. """
  97. pass
  98. def get_required(self, key: str) -> Any:
  99. """
  100. Holt einen erforderlichen Secret-Wert.
  101. Args:
  102. key: Schlüssel des Secrets.
  103. Returns:
  104. Secret-Wert.
  105. Raises:
  106. KeyError: Wenn das Secret nicht gefunden wird.
  107. """
  108. value = self.get(key)
  109. if value is None:
  110. raise KeyError(f"Erforderliches Secret '{key}' nicht gefunden")
  111. return value
  112. def set(self, key: str, value: Any) -> bool:
  113. """
  114. Speichert einen Secret-Wert.
  115. Args:
  116. key: Schlüssel.
  117. value: Wert.
  118. Returns:
  119. True wenn erfolgreich.
  120. Note:
  121. Nicht alle Provider unterstützen Schreibzugriff.
  122. """
  123. raise NotImplementedError(
  124. f"{self._name} unterstützt kein Speichern von Secrets"
  125. )
  126. def delete(self, key: str) -> bool:
  127. """
  128. Löscht ein Secret.
  129. Args:
  130. key: Schlüssel.
  131. Returns:
  132. True wenn erfolgreich.
  133. """
  134. raise NotImplementedError(
  135. f"{self._name} unterstützt kein Löschen von Secrets"
  136. )
  137. def exists(self, key: str) -> bool:
  138. """
  139. Prüft, ob ein Secret existiert.
  140. Args:
  141. key: Schlüssel.
  142. Returns:
  143. True wenn vorhanden.
  144. """
  145. return self.get(key) is not None
  146. def list_keys(self) -> list[str]:
  147. """
  148. Listet alle verfügbaren Secret-Schlüssel.
  149. Returns:
  150. Liste der Schlüssel.
  151. """
  152. raise NotImplementedError(
  153. f"{self._name} unterstützt kein Auflisten von Secrets"
  154. )
  155. def clear_cache(self) -> None:
  156. """Löscht den Secret-Cache."""
  157. self._cache.clear()
  158. def refresh(self, key: str | None = None) -> None:
  159. """
  160. Aktualisiert gecachte Secrets.
  161. Args:
  162. key: Optionaler spezifischer Schlüssel.
  163. """
  164. if key:
  165. self._cache.pop(key, None)
  166. else:
  167. self._cache.clear()
  168. class ChainedSecretsProvider(SecretsProvider):
  169. """
  170. Provider der mehrere Provider verkettet.
  171. Sucht Secrets in der Reihenfolge der registrierten Provider
  172. und gibt den ersten Treffer zurück.
  173. """
  174. def __init__(
  175. self,
  176. providers: list[SecretsProvider] | None = None,
  177. name: str = "ChainedSecretsProvider",
  178. ) -> None:
  179. """
  180. Initialisiert den verketteten Provider.
  181. Args:
  182. providers: Liste von Providern.
  183. name: Name des Providers.
  184. """
  185. super().__init__(name)
  186. self._providers = providers or []
  187. def add_provider(self, provider: SecretsProvider) -> "ChainedSecretsProvider":
  188. """
  189. Fügt einen Provider zur Kette hinzu.
  190. Args:
  191. provider: Der hinzuzufügende Provider.
  192. Returns:
  193. Self für Method-Chaining.
  194. """
  195. self._providers.append(provider)
  196. return self
  197. def get(self, key: str, default: T | None = None) -> T | None:
  198. """Sucht in allen Providern nach dem Secret."""
  199. for provider in self._providers:
  200. value = provider.get(key)
  201. if value is not None:
  202. return value
  203. return default
  204. def get_secret(self, key: str) -> SecretValue | None:
  205. """Sucht in allen Providern nach dem Secret mit Metadaten."""
  206. for provider in self._providers:
  207. secret = provider.get_secret(key)
  208. if secret is not None:
  209. return secret
  210. return None
  211. def exists(self, key: str) -> bool:
  212. """Prüft in allen Providern."""
  213. return any(p.exists(key) for p in self._providers)
  214. def list_keys(self) -> list[str]:
  215. """Sammelt Schlüssel aus allen Providern."""
  216. keys: set[str] = set()
  217. for provider in self._providers:
  218. try:
  219. keys.update(provider.list_keys())
  220. except NotImplementedError:
  221. pass
  222. return sorted(keys)
  223. class CachingSecretsProvider(SecretsProvider):
  224. """
  225. Wrapper der Secrets cached.
  226. Reduziert Zugriffe auf den zugrunde liegenden Provider
  227. durch zeitbasiertes Caching.
  228. """
  229. def __init__(
  230. self,
  231. provider: SecretsProvider,
  232. ttl_seconds: float = 300.0,
  233. name: str = "",
  234. ) -> None:
  235. """
  236. Initialisiert den Caching-Wrapper.
  237. Args:
  238. provider: Der zugrunde liegende Provider.
  239. ttl_seconds: Cache-TTL in Sekunden.
  240. name: Name des Providers.
  241. """
  242. super().__init__(name or f"Cached({provider.name})")
  243. self._provider = provider
  244. self._ttl_seconds = ttl_seconds
  245. self._cache_times: dict[str, datetime] = {}
  246. def _is_cache_valid(self, key: str) -> bool:
  247. """Prüft, ob der Cache-Eintrag noch gültig ist."""
  248. if key not in self._cache or key not in self._cache_times:
  249. return False
  250. from datetime import timedelta
  251. age = datetime.now() - self._cache_times[key]
  252. return age.total_seconds() < self._ttl_seconds
  253. def get(self, key: str, default: T | None = None) -> T | None:
  254. """Holt aus Cache oder Provider."""
  255. if self._cache_enabled and self._is_cache_valid(key):
  256. return self._cache[key].value
  257. value = self._provider.get(key, default)
  258. if value is not None and self._cache_enabled:
  259. self._cache[key] = SecretValue(
  260. key=key,
  261. value=value,
  262. source=self._provider.name,
  263. )
  264. self._cache_times[key] = datetime.now()
  265. return value
  266. def get_secret(self, key: str) -> SecretValue | None:
  267. """Holt Secret mit Metadaten aus Cache oder Provider."""
  268. if self._cache_enabled and self._is_cache_valid(key):
  269. return self._cache[key]
  270. secret = self._provider.get_secret(key)
  271. if secret is not None and self._cache_enabled:
  272. self._cache[key] = secret
  273. self._cache_times[key] = datetime.now()
  274. return secret
  275. def clear_cache(self) -> None:
  276. """Löscht den Cache."""
  277. self._cache.clear()
  278. self._cache_times.clear()
  279. def exists(self, key: str) -> bool:
  280. """Prüft Existenz."""
  281. if self._cache_enabled and key in self._cache:
  282. return True
  283. return self._provider.exists(key)
  284. def list_keys(self) -> list[str]:
  285. """Listet alle Schlüssel."""
  286. return self._provider.list_keys()