| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554 |
- # -*- coding: utf-8 -*-
- """
- Access Control mit Whitelist/Blacklist Unterstützung.
- Bietet flexible Zugriffskontrolle basierend auf IP-Adressen,
- CIDR-Bereichen und MAC-Adressen.
- """
- import ipaddress
- import re
- from abc import ABC, abstractmethod
- from dataclasses import dataclass, field
- from datetime import datetime
- from enum import IntEnum, auto
- from typing import Any
- class AccessDecision(IntEnum):
- """Entscheidung der Zugriffskontrolle."""
- ALLOW = auto() # Zugriff erlaubt
- DENY = auto() # Zugriff verweigert
- NEUTRAL = auto() # Keine Entscheidung (nächste Regel prüfen)
- @dataclass
- class AccessRule:
- """
- Einzelne Zugriffsregel.
- Attributes:
- pattern: Das zu matchende Pattern (IP, CIDR, MAC, etc.)
- decision: Die Entscheidung bei Match
- description: Beschreibung der Regel
- expires_at: Optionales Ablaufdatum
- hit_count: Anzahl der Treffer
- created_at: Erstellungszeitpunkt
- """
- pattern: str
- decision: AccessDecision
- description: str = ""
- expires_at: datetime | None = None
- hit_count: int = 0
- created_at: datetime = field(default_factory=datetime.now)
- def is_expired(self) -> bool:
- """Prüft, ob die Regel abgelaufen ist."""
- if self.expires_at is None:
- return False
- return datetime.now() > self.expires_at
- def record_hit(self) -> None:
- """Zeichnet einen Treffer auf."""
- self.hit_count += 1
- class Matcher(ABC):
- """Basisklasse für Pattern-Matcher."""
- @abstractmethod
- def matches(self, value: str) -> bool:
- """Prüft, ob der Wert dem Pattern entspricht."""
- ...
- class IPMatcher(Matcher):
- """
- Matcher für einzelne IP-Adressen.
- Unterstützt IPv4 und IPv6.
- """
- def __init__(self, ip: str) -> None:
- """
- Initialisiert den IP-Matcher.
- Args:
- ip: Die zu matchende IP-Adresse
- """
- self._ip = ipaddress.ip_address(ip)
- def matches(self, value: str) -> bool:
- """Prüft, ob die IP-Adresse übereinstimmt."""
- try:
- check_ip = ipaddress.ip_address(value)
- return check_ip == self._ip
- except ValueError:
- return False
- class CIDRMatcher(Matcher):
- """
- Matcher für CIDR-Bereiche (IP-Netzwerke).
- Beispiel: 192.168.1.0/24 matcht alle IPs von 192.168.1.0 bis 192.168.1.255
- """
- def __init__(self, cidr: str) -> None:
- """
- Initialisiert den CIDR-Matcher.
- Args:
- cidr: CIDR-Notation (z.B. "192.168.1.0/24")
- """
- self._network = ipaddress.ip_network(cidr, strict=False)
- def matches(self, value: str) -> bool:
- """Prüft, ob die IP im Netzwerk liegt."""
- try:
- check_ip = ipaddress.ip_address(value)
- return check_ip in self._network
- except ValueError:
- return False
- class MACMatcher(Matcher):
- """
- Matcher für MAC-Adressen.
- Unterstützt verschiedene Formate:
- - AA:BB:CC:DD:EE:FF
- - AA-BB-CC-DD-EE-FF
- - AABBCCDDEEFF
- """
- # Regex für MAC-Adressen
- _MAC_PATTERN = re.compile(
- r'^([0-9A-Fa-f]{2}[:-]?){5}([0-9A-Fa-f]{2})$'
- )
- def __init__(self, mac: str) -> None:
- """
- Initialisiert den MAC-Matcher.
- Args:
- mac: Die zu matchende MAC-Adresse
- """
- self._mac = self._normalize_mac(mac)
- @staticmethod
- def _normalize_mac(mac: str) -> str:
- """Normalisiert eine MAC-Adresse zu Großbuchstaben ohne Trennzeichen."""
- return mac.upper().replace(":", "").replace("-", "")
- def matches(self, value: str) -> bool:
- """Prüft, ob die MAC-Adresse übereinstimmt."""
- if not self._MAC_PATTERN.match(value):
- return False
- return self._normalize_mac(value) == self._mac
- class WildcardMatcher(Matcher):
- """
- Matcher mit Wildcard-Unterstützung.
- Unterstützt * als Platzhalter für beliebige Zeichen.
- Beispiel: "192.168.*.*" matcht alle IPs im 192.168.x.x Bereich
- """
- def __init__(self, pattern: str) -> None:
- """
- Initialisiert den Wildcard-Matcher.
- Args:
- pattern: Pattern mit optionalen Wildcards (*)
- """
- # Konvertiere Wildcard-Pattern zu Regex
- escaped = re.escape(pattern)
- regex_pattern = escaped.replace(r'\*', '.*')
- self._regex = re.compile(f'^{regex_pattern}$')
- def matches(self, value: str) -> bool:
- """Prüft, ob der Wert dem Pattern entspricht."""
- return bool(self._regex.match(value))
- class AccessController:
- """
- Zentrale Zugriffskontrolle mit Whitelist/Blacklist.
- Verarbeitet Regeln in folgender Reihenfolge:
- 1. Blacklist (wenn Match → DENY)
- 2. Whitelist (wenn Match → ALLOW)
- 3. Default-Entscheidung
- Example:
- controller = AccessController(default_allow=False)
- # Whitelist: Lokales Netzwerk erlauben
- controller.whitelist("192.168.1.0/24", "Lokales Netzwerk")
- # Blacklist: Bestimmte IP sperren
- controller.blacklist("192.168.1.100", "Bekannter Angreifer")
- # Prüfung
- if controller.check_ip("192.168.1.50") == AccessDecision.ALLOW:
- print("Zugriff erlaubt")
- """
- def __init__(self, default_allow: bool = False) -> None:
- """
- Initialisiert die Zugriffskontrolle.
- Args:
- default_allow: Standardentscheidung wenn keine Regel matcht
- """
- self._default_allow = default_allow
- self._whitelist: list[tuple[Matcher, AccessRule]] = []
- self._blacklist: list[tuple[Matcher, AccessRule]] = []
- self._mac_whitelist: list[tuple[Matcher, AccessRule]] = []
- self._mac_blacklist: list[tuple[Matcher, AccessRule]] = []
- @property
- def default_decision(self) -> AccessDecision:
- """Standard-Entscheidung wenn keine Regel matcht."""
- return AccessDecision.ALLOW if self._default_allow else AccessDecision.DENY
- def _create_ip_matcher(self, pattern: str) -> Matcher:
- """Erstellt den passenden Matcher für ein IP-Pattern."""
- if "/" in pattern:
- return CIDRMatcher(pattern)
- elif "*" in pattern:
- return WildcardMatcher(pattern)
- else:
- return IPMatcher(pattern)
- def whitelist(
- self,
- pattern: str,
- description: str = "",
- expires_at: datetime | None = None
- ) -> AccessRule:
- """
- Fügt eine IP/CIDR zur Whitelist hinzu.
- Args:
- pattern: IP-Adresse, CIDR oder Wildcard-Pattern
- description: Beschreibung der Regel
- expires_at: Optionales Ablaufdatum
- Returns:
- Die erstellte Regel
- """
- rule = AccessRule(
- pattern=pattern,
- decision=AccessDecision.ALLOW,
- description=description,
- expires_at=expires_at
- )
- matcher = self._create_ip_matcher(pattern)
- self._whitelist.append((matcher, rule))
- return rule
- def blacklist(
- self,
- pattern: str,
- description: str = "",
- expires_at: datetime | None = None
- ) -> AccessRule:
- """
- Fügt eine IP/CIDR zur Blacklist hinzu.
- Args:
- pattern: IP-Adresse, CIDR oder Wildcard-Pattern
- description: Beschreibung der Regel
- expires_at: Optionales Ablaufdatum
- Returns:
- Die erstellte Regel
- """
- rule = AccessRule(
- pattern=pattern,
- decision=AccessDecision.DENY,
- description=description,
- expires_at=expires_at
- )
- matcher = self._create_ip_matcher(pattern)
- self._blacklist.append((matcher, rule))
- return rule
- def whitelist_mac(
- self,
- mac: str,
- description: str = "",
- expires_at: datetime | None = None
- ) -> AccessRule:
- """
- Fügt eine MAC-Adresse zur Whitelist hinzu.
- Args:
- mac: MAC-Adresse
- description: Beschreibung der Regel
- expires_at: Optionales Ablaufdatum
- Returns:
- Die erstellte Regel
- """
- rule = AccessRule(
- pattern=mac,
- decision=AccessDecision.ALLOW,
- description=description,
- expires_at=expires_at
- )
- matcher = MACMatcher(mac)
- self._mac_whitelist.append((matcher, rule))
- return rule
- def blacklist_mac(
- self,
- mac: str,
- description: str = "",
- expires_at: datetime | None = None
- ) -> AccessRule:
- """
- Fügt eine MAC-Adresse zur Blacklist hinzu.
- Args:
- mac: MAC-Adresse
- description: Beschreibung der Regel
- expires_at: Optionales Ablaufdatum
- Returns:
- Die erstellte Regel
- """
- rule = AccessRule(
- pattern=mac,
- decision=AccessDecision.DENY,
- description=description,
- expires_at=expires_at
- )
- matcher = MACMatcher(mac)
- self._mac_blacklist.append((matcher, rule))
- return rule
- def check_ip(self, ip: str) -> AccessDecision:
- """
- Prüft die Zugriffsberechtigung für eine IP-Adresse.
- Reihenfolge: Blacklist → Whitelist → Default
- Args:
- ip: Die zu prüfende IP-Adresse
- Returns:
- AccessDecision (ALLOW oder DENY)
- """
- # Blacklist zuerst prüfen
- for matcher, rule in self._blacklist:
- if rule.is_expired():
- continue
- if matcher.matches(ip):
- rule.record_hit()
- return AccessDecision.DENY
- # Dann Whitelist prüfen
- for matcher, rule in self._whitelist:
- if rule.is_expired():
- continue
- if matcher.matches(ip):
- rule.record_hit()
- return AccessDecision.ALLOW
- # Default-Entscheidung
- return self.default_decision
- def check_mac(self, mac: str) -> AccessDecision:
- """
- Prüft die Zugriffsberechtigung für eine MAC-Adresse.
- Args:
- mac: Die zu prüfende MAC-Adresse
- Returns:
- AccessDecision (ALLOW oder DENY)
- """
- # Blacklist zuerst prüfen
- for matcher, rule in self._mac_blacklist:
- if rule.is_expired():
- continue
- if matcher.matches(mac):
- rule.record_hit()
- return AccessDecision.DENY
- # Dann Whitelist prüfen
- for matcher, rule in self._mac_whitelist:
- if rule.is_expired():
- continue
- if matcher.matches(mac):
- rule.record_hit()
- return AccessDecision.ALLOW
- # Default-Entscheidung
- return self.default_decision
- def check(self, ip: str, mac: str | None = None) -> AccessDecision:
- """
- Kombinierte Prüfung von IP und optional MAC.
- Beide müssen erlaubt sein (wenn MAC angegeben).
- Args:
- ip: IP-Adresse
- mac: Optionale MAC-Adresse
- Returns:
- AccessDecision (ALLOW oder DENY)
- """
- ip_decision = self.check_ip(ip)
- if ip_decision == AccessDecision.DENY:
- return AccessDecision.DENY
- if mac is not None:
- mac_decision = self.check_mac(mac)
- if mac_decision == AccessDecision.DENY:
- return AccessDecision.DENY
- return AccessDecision.ALLOW
- def remove_from_whitelist(self, pattern: str) -> bool:
- """Entfernt ein Pattern von der IP-Whitelist."""
- for i, (_, rule) in enumerate(self._whitelist):
- if rule.pattern == pattern:
- self._whitelist.pop(i)
- return True
- return False
- def remove_from_blacklist(self, pattern: str) -> bool:
- """Entfernt ein Pattern von der IP-Blacklist."""
- for i, (_, rule) in enumerate(self._blacklist):
- if rule.pattern == pattern:
- self._blacklist.pop(i)
- return True
- return False
- def clear_expired(self) -> int:
- """
- Entfernt alle abgelaufenen Regeln.
- Returns:
- Anzahl entfernter Regeln
- """
- count = 0
- for rules_list in (
- self._whitelist,
- self._blacklist,
- self._mac_whitelist,
- self._mac_blacklist
- ):
- expired = [
- (m, r) for m, r in rules_list if r.is_expired()
- ]
- for item in expired:
- rules_list.remove(item)
- count += 1
- return count
- def get_statistics(self) -> dict[str, Any]:
- """
- Gibt Statistiken über die Zugriffskontrolle zurück.
- Returns:
- Dictionary mit Statistiken
- """
- return {
- "whitelist_count": len(self._whitelist),
- "blacklist_count": len(self._blacklist),
- "mac_whitelist_count": len(self._mac_whitelist),
- "mac_blacklist_count": len(self._mac_blacklist),
- "default_allow": self._default_allow,
- "whitelist_hits": sum(r.hit_count for _, r in self._whitelist),
- "blacklist_hits": sum(r.hit_count for _, r in self._blacklist),
- }
- def to_dict(self) -> dict[str, Any]:
- """
- Serialisiert die Regeln zu einem Dictionary.
- Returns:
- Dictionary mit allen Regeln
- """
- def rule_to_dict(rule: AccessRule) -> dict[str, Any]:
- return {
- "pattern": rule.pattern,
- "decision": rule.decision.name,
- "description": rule.description,
- "expires_at": rule.expires_at.isoformat() if rule.expires_at else None,
- "hit_count": rule.hit_count,
- "created_at": rule.created_at.isoformat(),
- }
- return {
- "default_allow": self._default_allow,
- "ip_whitelist": [rule_to_dict(r) for _, r in self._whitelist],
- "ip_blacklist": [rule_to_dict(r) for _, r in self._blacklist],
- "mac_whitelist": [rule_to_dict(r) for _, r in self._mac_whitelist],
- "mac_blacklist": [rule_to_dict(r) for _, r in self._mac_blacklist],
- }
- @classmethod
- def from_dict(cls, data: dict[str, Any]) -> "AccessController":
- """
- Erstellt einen AccessController aus einem Dictionary.
- Args:
- data: Serialisierte Regeln
- Returns:
- Neuer AccessController
- """
- controller = cls(default_allow=data.get("default_allow", False))
- for rule_data in data.get("ip_whitelist", []):
- expires = None
- if rule_data.get("expires_at"):
- expires = datetime.fromisoformat(rule_data["expires_at"])
- controller.whitelist(
- rule_data["pattern"],
- rule_data.get("description", ""),
- expires
- )
- for rule_data in data.get("ip_blacklist", []):
- expires = None
- if rule_data.get("expires_at"):
- expires = datetime.fromisoformat(rule_data["expires_at"])
- controller.blacklist(
- rule_data["pattern"],
- rule_data.get("description", ""),
- expires
- )
- for rule_data in data.get("mac_whitelist", []):
- expires = None
- if rule_data.get("expires_at"):
- expires = datetime.fromisoformat(rule_data["expires_at"])
- controller.whitelist_mac(
- rule_data["pattern"],
- rule_data.get("description", ""),
- expires
- )
- for rule_data in data.get("mac_blacklist", []):
- expires = None
- if rule_data.get("expires_at"):
- expires = datetime.fromisoformat(rule_data["expires_at"])
- controller.blacklist_mac(
- rule_data["pattern"],
- rule_data.get("description", ""),
- expires
- )
- return controller
|