access_control.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. # -*- coding: utf-8 -*-
  2. """
  3. Access Control mit Whitelist/Blacklist Unterstützung.
  4. Bietet flexible Zugriffskontrolle basierend auf IP-Adressen,
  5. CIDR-Bereichen und MAC-Adressen.
  6. """
  7. import ipaddress
  8. import re
  9. from abc import ABC, abstractmethod
  10. from dataclasses import dataclass, field
  11. from datetime import datetime
  12. from enum import IntEnum, auto
  13. from typing import Any
  14. class AccessDecision(IntEnum):
  15. """Entscheidung der Zugriffskontrolle."""
  16. ALLOW = auto() # Zugriff erlaubt
  17. DENY = auto() # Zugriff verweigert
  18. NEUTRAL = auto() # Keine Entscheidung (nächste Regel prüfen)
  19. @dataclass
  20. class AccessRule:
  21. """
  22. Einzelne Zugriffsregel.
  23. Attributes:
  24. pattern: Das zu matchende Pattern (IP, CIDR, MAC, etc.)
  25. decision: Die Entscheidung bei Match
  26. description: Beschreibung der Regel
  27. expires_at: Optionales Ablaufdatum
  28. hit_count: Anzahl der Treffer
  29. created_at: Erstellungszeitpunkt
  30. """
  31. pattern: str
  32. decision: AccessDecision
  33. description: str = ""
  34. expires_at: datetime | None = None
  35. hit_count: int = 0
  36. created_at: datetime = field(default_factory=datetime.now)
  37. def is_expired(self) -> bool:
  38. """Prüft, ob die Regel abgelaufen ist."""
  39. if self.expires_at is None:
  40. return False
  41. return datetime.now() > self.expires_at
  42. def record_hit(self) -> None:
  43. """Zeichnet einen Treffer auf."""
  44. self.hit_count += 1
  45. class Matcher(ABC):
  46. """Basisklasse für Pattern-Matcher."""
  47. @abstractmethod
  48. def matches(self, value: str) -> bool:
  49. """Prüft, ob der Wert dem Pattern entspricht."""
  50. ...
  51. class IPMatcher(Matcher):
  52. """
  53. Matcher für einzelne IP-Adressen.
  54. Unterstützt IPv4 und IPv6.
  55. """
  56. def __init__(self, ip: str) -> None:
  57. """
  58. Initialisiert den IP-Matcher.
  59. Args:
  60. ip: Die zu matchende IP-Adresse
  61. """
  62. self._ip = ipaddress.ip_address(ip)
  63. def matches(self, value: str) -> bool:
  64. """Prüft, ob die IP-Adresse übereinstimmt."""
  65. try:
  66. check_ip = ipaddress.ip_address(value)
  67. return check_ip == self._ip
  68. except ValueError:
  69. return False
  70. class CIDRMatcher(Matcher):
  71. """
  72. Matcher für CIDR-Bereiche (IP-Netzwerke).
  73. Beispiel: 192.168.1.0/24 matcht alle IPs von 192.168.1.0 bis 192.168.1.255
  74. """
  75. def __init__(self, cidr: str) -> None:
  76. """
  77. Initialisiert den CIDR-Matcher.
  78. Args:
  79. cidr: CIDR-Notation (z.B. "192.168.1.0/24")
  80. """
  81. self._network = ipaddress.ip_network(cidr, strict=False)
  82. def matches(self, value: str) -> bool:
  83. """Prüft, ob die IP im Netzwerk liegt."""
  84. try:
  85. check_ip = ipaddress.ip_address(value)
  86. return check_ip in self._network
  87. except ValueError:
  88. return False
  89. class MACMatcher(Matcher):
  90. """
  91. Matcher für MAC-Adressen.
  92. Unterstützt verschiedene Formate:
  93. - AA:BB:CC:DD:EE:FF
  94. - AA-BB-CC-DD-EE-FF
  95. - AABBCCDDEEFF
  96. """
  97. # Regex für MAC-Adressen
  98. _MAC_PATTERN = re.compile(
  99. r'^([0-9A-Fa-f]{2}[:-]?){5}([0-9A-Fa-f]{2})$'
  100. )
  101. def __init__(self, mac: str) -> None:
  102. """
  103. Initialisiert den MAC-Matcher.
  104. Args:
  105. mac: Die zu matchende MAC-Adresse
  106. """
  107. self._mac = self._normalize_mac(mac)
  108. @staticmethod
  109. def _normalize_mac(mac: str) -> str:
  110. """Normalisiert eine MAC-Adresse zu Großbuchstaben ohne Trennzeichen."""
  111. return mac.upper().replace(":", "").replace("-", "")
  112. def matches(self, value: str) -> bool:
  113. """Prüft, ob die MAC-Adresse übereinstimmt."""
  114. if not self._MAC_PATTERN.match(value):
  115. return False
  116. return self._normalize_mac(value) == self._mac
  117. class WildcardMatcher(Matcher):
  118. """
  119. Matcher mit Wildcard-Unterstützung.
  120. Unterstützt * als Platzhalter für beliebige Zeichen.
  121. Beispiel: "192.168.*.*" matcht alle IPs im 192.168.x.x Bereich
  122. """
  123. def __init__(self, pattern: str) -> None:
  124. """
  125. Initialisiert den Wildcard-Matcher.
  126. Args:
  127. pattern: Pattern mit optionalen Wildcards (*)
  128. """
  129. # Konvertiere Wildcard-Pattern zu Regex
  130. escaped = re.escape(pattern)
  131. regex_pattern = escaped.replace(r'\*', '.*')
  132. self._regex = re.compile(f'^{regex_pattern}$')
  133. def matches(self, value: str) -> bool:
  134. """Prüft, ob der Wert dem Pattern entspricht."""
  135. return bool(self._regex.match(value))
  136. class AccessController:
  137. """
  138. Zentrale Zugriffskontrolle mit Whitelist/Blacklist.
  139. Verarbeitet Regeln in folgender Reihenfolge:
  140. 1. Blacklist (wenn Match → DENY)
  141. 2. Whitelist (wenn Match → ALLOW)
  142. 3. Default-Entscheidung
  143. Example:
  144. controller = AccessController(default_allow=False)
  145. # Whitelist: Lokales Netzwerk erlauben
  146. controller.whitelist("192.168.1.0/24", "Lokales Netzwerk")
  147. # Blacklist: Bestimmte IP sperren
  148. controller.blacklist("192.168.1.100", "Bekannter Angreifer")
  149. # Prüfung
  150. if controller.check_ip("192.168.1.50") == AccessDecision.ALLOW:
  151. print("Zugriff erlaubt")
  152. """
  153. def __init__(self, default_allow: bool = False) -> None:
  154. """
  155. Initialisiert die Zugriffskontrolle.
  156. Args:
  157. default_allow: Standardentscheidung wenn keine Regel matcht
  158. """
  159. self._default_allow = default_allow
  160. self._whitelist: list[tuple[Matcher, AccessRule]] = []
  161. self._blacklist: list[tuple[Matcher, AccessRule]] = []
  162. self._mac_whitelist: list[tuple[Matcher, AccessRule]] = []
  163. self._mac_blacklist: list[tuple[Matcher, AccessRule]] = []
  164. @property
  165. def default_decision(self) -> AccessDecision:
  166. """Standard-Entscheidung wenn keine Regel matcht."""
  167. return AccessDecision.ALLOW if self._default_allow else AccessDecision.DENY
  168. def _create_ip_matcher(self, pattern: str) -> Matcher:
  169. """Erstellt den passenden Matcher für ein IP-Pattern."""
  170. if "/" in pattern:
  171. return CIDRMatcher(pattern)
  172. elif "*" in pattern:
  173. return WildcardMatcher(pattern)
  174. else:
  175. return IPMatcher(pattern)
  176. def whitelist(
  177. self,
  178. pattern: str,
  179. description: str = "",
  180. expires_at: datetime | None = None
  181. ) -> AccessRule:
  182. """
  183. Fügt eine IP/CIDR zur Whitelist hinzu.
  184. Args:
  185. pattern: IP-Adresse, CIDR oder Wildcard-Pattern
  186. description: Beschreibung der Regel
  187. expires_at: Optionales Ablaufdatum
  188. Returns:
  189. Die erstellte Regel
  190. """
  191. rule = AccessRule(
  192. pattern=pattern,
  193. decision=AccessDecision.ALLOW,
  194. description=description,
  195. expires_at=expires_at
  196. )
  197. matcher = self._create_ip_matcher(pattern)
  198. self._whitelist.append((matcher, rule))
  199. return rule
  200. def blacklist(
  201. self,
  202. pattern: str,
  203. description: str = "",
  204. expires_at: datetime | None = None
  205. ) -> AccessRule:
  206. """
  207. Fügt eine IP/CIDR zur Blacklist hinzu.
  208. Args:
  209. pattern: IP-Adresse, CIDR oder Wildcard-Pattern
  210. description: Beschreibung der Regel
  211. expires_at: Optionales Ablaufdatum
  212. Returns:
  213. Die erstellte Regel
  214. """
  215. rule = AccessRule(
  216. pattern=pattern,
  217. decision=AccessDecision.DENY,
  218. description=description,
  219. expires_at=expires_at
  220. )
  221. matcher = self._create_ip_matcher(pattern)
  222. self._blacklist.append((matcher, rule))
  223. return rule
  224. def whitelist_mac(
  225. self,
  226. mac: str,
  227. description: str = "",
  228. expires_at: datetime | None = None
  229. ) -> AccessRule:
  230. """
  231. Fügt eine MAC-Adresse zur Whitelist hinzu.
  232. Args:
  233. mac: MAC-Adresse
  234. description: Beschreibung der Regel
  235. expires_at: Optionales Ablaufdatum
  236. Returns:
  237. Die erstellte Regel
  238. """
  239. rule = AccessRule(
  240. pattern=mac,
  241. decision=AccessDecision.ALLOW,
  242. description=description,
  243. expires_at=expires_at
  244. )
  245. matcher = MACMatcher(mac)
  246. self._mac_whitelist.append((matcher, rule))
  247. return rule
  248. def blacklist_mac(
  249. self,
  250. mac: str,
  251. description: str = "",
  252. expires_at: datetime | None = None
  253. ) -> AccessRule:
  254. """
  255. Fügt eine MAC-Adresse zur Blacklist hinzu.
  256. Args:
  257. mac: MAC-Adresse
  258. description: Beschreibung der Regel
  259. expires_at: Optionales Ablaufdatum
  260. Returns:
  261. Die erstellte Regel
  262. """
  263. rule = AccessRule(
  264. pattern=mac,
  265. decision=AccessDecision.DENY,
  266. description=description,
  267. expires_at=expires_at
  268. )
  269. matcher = MACMatcher(mac)
  270. self._mac_blacklist.append((matcher, rule))
  271. return rule
  272. def check_ip(self, ip: str) -> AccessDecision:
  273. """
  274. Prüft die Zugriffsberechtigung für eine IP-Adresse.
  275. Reihenfolge: Blacklist → Whitelist → Default
  276. Args:
  277. ip: Die zu prüfende IP-Adresse
  278. Returns:
  279. AccessDecision (ALLOW oder DENY)
  280. """
  281. # Blacklist zuerst prüfen
  282. for matcher, rule in self._blacklist:
  283. if rule.is_expired():
  284. continue
  285. if matcher.matches(ip):
  286. rule.record_hit()
  287. return AccessDecision.DENY
  288. # Dann Whitelist prüfen
  289. for matcher, rule in self._whitelist:
  290. if rule.is_expired():
  291. continue
  292. if matcher.matches(ip):
  293. rule.record_hit()
  294. return AccessDecision.ALLOW
  295. # Default-Entscheidung
  296. return self.default_decision
  297. def check_mac(self, mac: str) -> AccessDecision:
  298. """
  299. Prüft die Zugriffsberechtigung für eine MAC-Adresse.
  300. Args:
  301. mac: Die zu prüfende MAC-Adresse
  302. Returns:
  303. AccessDecision (ALLOW oder DENY)
  304. """
  305. # Blacklist zuerst prüfen
  306. for matcher, rule in self._mac_blacklist:
  307. if rule.is_expired():
  308. continue
  309. if matcher.matches(mac):
  310. rule.record_hit()
  311. return AccessDecision.DENY
  312. # Dann Whitelist prüfen
  313. for matcher, rule in self._mac_whitelist:
  314. if rule.is_expired():
  315. continue
  316. if matcher.matches(mac):
  317. rule.record_hit()
  318. return AccessDecision.ALLOW
  319. # Default-Entscheidung
  320. return self.default_decision
  321. def check(self, ip: str, mac: str | None = None) -> AccessDecision:
  322. """
  323. Kombinierte Prüfung von IP und optional MAC.
  324. Beide müssen erlaubt sein (wenn MAC angegeben).
  325. Args:
  326. ip: IP-Adresse
  327. mac: Optionale MAC-Adresse
  328. Returns:
  329. AccessDecision (ALLOW oder DENY)
  330. """
  331. ip_decision = self.check_ip(ip)
  332. if ip_decision == AccessDecision.DENY:
  333. return AccessDecision.DENY
  334. if mac is not None:
  335. mac_decision = self.check_mac(mac)
  336. if mac_decision == AccessDecision.DENY:
  337. return AccessDecision.DENY
  338. return AccessDecision.ALLOW
  339. def remove_from_whitelist(self, pattern: str) -> bool:
  340. """Entfernt ein Pattern von der IP-Whitelist."""
  341. for i, (_, rule) in enumerate(self._whitelist):
  342. if rule.pattern == pattern:
  343. self._whitelist.pop(i)
  344. return True
  345. return False
  346. def remove_from_blacklist(self, pattern: str) -> bool:
  347. """Entfernt ein Pattern von der IP-Blacklist."""
  348. for i, (_, rule) in enumerate(self._blacklist):
  349. if rule.pattern == pattern:
  350. self._blacklist.pop(i)
  351. return True
  352. return False
  353. def clear_expired(self) -> int:
  354. """
  355. Entfernt alle abgelaufenen Regeln.
  356. Returns:
  357. Anzahl entfernter Regeln
  358. """
  359. count = 0
  360. for rules_list in (
  361. self._whitelist,
  362. self._blacklist,
  363. self._mac_whitelist,
  364. self._mac_blacklist
  365. ):
  366. expired = [
  367. (m, r) for m, r in rules_list if r.is_expired()
  368. ]
  369. for item in expired:
  370. rules_list.remove(item)
  371. count += 1
  372. return count
  373. def get_statistics(self) -> dict[str, Any]:
  374. """
  375. Gibt Statistiken über die Zugriffskontrolle zurück.
  376. Returns:
  377. Dictionary mit Statistiken
  378. """
  379. return {
  380. "whitelist_count": len(self._whitelist),
  381. "blacklist_count": len(self._blacklist),
  382. "mac_whitelist_count": len(self._mac_whitelist),
  383. "mac_blacklist_count": len(self._mac_blacklist),
  384. "default_allow": self._default_allow,
  385. "whitelist_hits": sum(r.hit_count for _, r in self._whitelist),
  386. "blacklist_hits": sum(r.hit_count for _, r in self._blacklist),
  387. }
  388. def to_dict(self) -> dict[str, Any]:
  389. """
  390. Serialisiert die Regeln zu einem Dictionary.
  391. Returns:
  392. Dictionary mit allen Regeln
  393. """
  394. def rule_to_dict(rule: AccessRule) -> dict[str, Any]:
  395. return {
  396. "pattern": rule.pattern,
  397. "decision": rule.decision.name,
  398. "description": rule.description,
  399. "expires_at": rule.expires_at.isoformat() if rule.expires_at else None,
  400. "hit_count": rule.hit_count,
  401. "created_at": rule.created_at.isoformat(),
  402. }
  403. return {
  404. "default_allow": self._default_allow,
  405. "ip_whitelist": [rule_to_dict(r) for _, r in self._whitelist],
  406. "ip_blacklist": [rule_to_dict(r) for _, r in self._blacklist],
  407. "mac_whitelist": [rule_to_dict(r) for _, r in self._mac_whitelist],
  408. "mac_blacklist": [rule_to_dict(r) for _, r in self._mac_blacklist],
  409. }
  410. @classmethod
  411. def from_dict(cls, data: dict[str, Any]) -> "AccessController":
  412. """
  413. Erstellt einen AccessController aus einem Dictionary.
  414. Args:
  415. data: Serialisierte Regeln
  416. Returns:
  417. Neuer AccessController
  418. """
  419. controller = cls(default_allow=data.get("default_allow", False))
  420. for rule_data in data.get("ip_whitelist", []):
  421. expires = None
  422. if rule_data.get("expires_at"):
  423. expires = datetime.fromisoformat(rule_data["expires_at"])
  424. controller.whitelist(
  425. rule_data["pattern"],
  426. rule_data.get("description", ""),
  427. expires
  428. )
  429. for rule_data in data.get("ip_blacklist", []):
  430. expires = None
  431. if rule_data.get("expires_at"):
  432. expires = datetime.fromisoformat(rule_data["expires_at"])
  433. controller.blacklist(
  434. rule_data["pattern"],
  435. rule_data.get("description", ""),
  436. expires
  437. )
  438. for rule_data in data.get("mac_whitelist", []):
  439. expires = None
  440. if rule_data.get("expires_at"):
  441. expires = datetime.fromisoformat(rule_data["expires_at"])
  442. controller.whitelist_mac(
  443. rule_data["pattern"],
  444. rule_data.get("description", ""),
  445. expires
  446. )
  447. for rule_data in data.get("mac_blacklist", []):
  448. expires = None
  449. if rule_data.get("expires_at"):
  450. expires = datetime.fromisoformat(rule_data["expires_at"])
  451. controller.blacklist_mac(
  452. rule_data["pattern"],
  453. rule_data.get("description", ""),
  454. expires
  455. )
  456. return controller