-
-
Notifications
You must be signed in to change notification settings - Fork 874
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
events: add ASN Database reader (#7793)
* events: add ASN Database reader Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix test config generator Signed-off-by: Jens Langhammer <jens@goauthentik.io> * de-duplicate code Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add enrich_context Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rename to context processors? Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix cache Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use config deprecation system, update docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update more docs and tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add test asn db Signed-off-by: Jens Langhammer <jens@goauthentik.io> * re-build schema with latest versions Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
- Loading branch information
Showing
27 changed files
with
393 additions
and
143 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
"""ASN Enricher""" | ||
from typing import TYPE_CHECKING, Optional, TypedDict | ||
|
||
from django.http import HttpRequest | ||
from geoip2.errors import GeoIP2Error | ||
from geoip2.models import ASN | ||
from sentry_sdk import Hub | ||
|
||
from authentik.events.context_processors.mmdb import MMDBContextProcessor | ||
from authentik.lib.config import CONFIG | ||
from authentik.root.middleware import ClientIPMiddleware | ||
|
||
if TYPE_CHECKING: | ||
from authentik.api.v3.config import Capabilities | ||
from authentik.events.models import Event | ||
|
||
|
||
class ASNDict(TypedDict): | ||
"""ASN Details""" | ||
|
||
asn: int | ||
as_org: str | None | ||
network: str | None | ||
|
||
|
||
class ASNContextProcessor(MMDBContextProcessor): | ||
"""ASN Database reader wrapper""" | ||
|
||
def capability(self) -> Optional["Capabilities"]: | ||
from authentik.api.v3.config import Capabilities | ||
|
||
return Capabilities.CAN_ASN | ||
|
||
def path(self) -> str | None: | ||
return CONFIG.get("events.context_processors.asn") | ||
|
||
def enrich_event(self, event: "Event"): | ||
asn = self.asn_dict(event.client_ip) | ||
if not asn: | ||
return | ||
event.context["asn"] = asn | ||
|
||
def enrich_context(self, request: HttpRequest) -> dict: | ||
return { | ||
"asn": self.asn_dict(ClientIPMiddleware.get_client_ip(request)), | ||
} | ||
|
||
def asn(self, ip_address: str) -> Optional[ASN]: | ||
"""Wrapper for Reader.asn""" | ||
with Hub.current.start_span( | ||
op="authentik.events.asn.asn", | ||
description=ip_address, | ||
): | ||
if not self.enabled: | ||
return None | ||
self.check_expired() | ||
try: | ||
return self.reader.asn(ip_address) | ||
except (GeoIP2Error, ValueError): | ||
return None | ||
|
||
def asn_to_dict(self, asn: ASN) -> ASNDict: | ||
"""Convert ASN to dict""" | ||
asn_dict: ASNDict = { | ||
"asn": asn.autonomous_system_number, | ||
"as_org": asn.autonomous_system_organization, | ||
"network": str(asn.network) if asn.network else None, | ||
} | ||
return asn_dict | ||
|
||
def asn_dict(self, ip_address: str) -> Optional[ASNDict]: | ||
"""Wrapper for self.asn that returns a dict""" | ||
asn = self.asn(ip_address) | ||
if not asn: | ||
return None | ||
return self.asn_to_dict(asn) | ||
|
||
|
||
ASN_CONTEXT_PROCESSOR = ASNContextProcessor() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
"""Base event enricher""" | ||
from functools import cache | ||
from typing import TYPE_CHECKING, Optional | ||
|
||
from django.http import HttpRequest | ||
|
||
if TYPE_CHECKING: | ||
from authentik.api.v3.config import Capabilities | ||
from authentik.events.models import Event | ||
|
||
|
||
class EventContextProcessor: | ||
"""Base event enricher""" | ||
|
||
def capability(self) -> Optional["Capabilities"]: | ||
"""Return the capability this context processor provides""" | ||
return None | ||
|
||
def configured(self) -> bool: | ||
"""Return true if this context processor is configured""" | ||
return False | ||
|
||
def enrich_event(self, event: "Event"): | ||
"""Modify event""" | ||
raise NotImplementedError | ||
|
||
def enrich_context(self, request: HttpRequest) -> dict: | ||
"""Modify context""" | ||
raise NotImplementedError | ||
|
||
|
||
@cache | ||
def get_context_processors() -> list[EventContextProcessor]: | ||
"""Get a list of all configured context processors""" | ||
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR | ||
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR | ||
|
||
processors_types = [ASN_CONTEXT_PROCESSOR, GEOIP_CONTEXT_PROCESSOR] | ||
processors = [] | ||
for _type in processors_types: | ||
if _type.configured(): | ||
processors.append(_type) | ||
return processors |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
"""events GeoIP Reader""" | ||
from typing import TYPE_CHECKING, Optional, TypedDict | ||
|
||
from django.http import HttpRequest | ||
from geoip2.errors import GeoIP2Error | ||
from geoip2.models import City | ||
from sentry_sdk.hub import Hub | ||
|
||
from authentik.events.context_processors.mmdb import MMDBContextProcessor | ||
from authentik.lib.config import CONFIG | ||
from authentik.root.middleware import ClientIPMiddleware | ||
|
||
if TYPE_CHECKING: | ||
from authentik.api.v3.config import Capabilities | ||
from authentik.events.models import Event | ||
|
||
|
||
class GeoIPDict(TypedDict): | ||
"""GeoIP Details""" | ||
|
||
continent: str | ||
country: str | ||
lat: float | ||
long: float | ||
city: str | ||
|
||
|
||
class GeoIPContextProcessor(MMDBContextProcessor): | ||
"""Slim wrapper around GeoIP API""" | ||
|
||
def capability(self) -> Optional["Capabilities"]: | ||
from authentik.api.v3.config import Capabilities | ||
|
||
return Capabilities.CAN_GEO_IP | ||
|
||
def path(self) -> str | None: | ||
return CONFIG.get("events.context_processors.geoip") | ||
|
||
def enrich_event(self, event: "Event"): | ||
city = self.city_dict(event.client_ip) | ||
if not city: | ||
return | ||
event.context["geo"] = city | ||
|
||
def enrich_context(self, request: HttpRequest) -> dict: | ||
# Different key `geoip` vs `geo` for legacy reasons | ||
return {"geoip": self.city(ClientIPMiddleware.get_client_ip(request))} | ||
|
||
def city(self, ip_address: str) -> Optional[City]: | ||
"""Wrapper for Reader.city""" | ||
with Hub.current.start_span( | ||
op="authentik.events.geo.city", | ||
description=ip_address, | ||
): | ||
if not self.enabled: | ||
return None | ||
self.check_expired() | ||
try: | ||
return self.reader.city(ip_address) | ||
except (GeoIP2Error, ValueError): | ||
return None | ||
|
||
def city_to_dict(self, city: City) -> GeoIPDict: | ||
"""Convert City to dict""" | ||
city_dict: GeoIPDict = { | ||
"continent": city.continent.code, | ||
"country": city.country.iso_code, | ||
"lat": city.location.latitude, | ||
"long": city.location.longitude, | ||
"city": "", | ||
} | ||
if city.city.name: | ||
city_dict["city"] = city.city.name | ||
return city_dict | ||
|
||
def city_dict(self, ip_address: str) -> Optional[GeoIPDict]: | ||
"""Wrapper for self.city that returns a dict""" | ||
city = self.city(ip_address) | ||
if not city: | ||
return None | ||
return self.city_to_dict(city) | ||
|
||
|
||
GEOIP_CONTEXT_PROCESSOR = GeoIPContextProcessor() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
"""Common logic for reading MMDB files""" | ||
from pathlib import Path | ||
from typing import Optional | ||
|
||
from geoip2.database import Reader | ||
from structlog.stdlib import get_logger | ||
|
||
from authentik.events.context_processors.base import EventContextProcessor | ||
|
||
|
||
class MMDBContextProcessor(EventContextProcessor): | ||
"""Common logic for reading MaxMind DB files, including re-loading if the file has changed""" | ||
|
||
def __init__(self): | ||
self.reader: Optional[Reader] = None | ||
self._last_mtime: float = 0.0 | ||
self.logger = get_logger() | ||
self.open() | ||
|
||
def path(self) -> str | None: | ||
"""Get the path to the MMDB file to load""" | ||
raise NotImplementedError | ||
|
||
def open(self): | ||
"""Get GeoIP Reader, if configured, otherwise none""" | ||
path = self.path() | ||
if path == "" or not path: | ||
return | ||
try: | ||
self.reader = Reader(path) | ||
self._last_mtime = Path(path).stat().st_mtime | ||
self.logger.info("Loaded MMDB database", last_write=self._last_mtime, file=path) | ||
except OSError as exc: | ||
self.logger.warning("Failed to load MMDB database", exc=exc) | ||
|
||
def check_expired(self): | ||
"""Check if the modification date of the MMDB database has | ||
changed, and reload it if so""" | ||
path = self.path() | ||
if path == "" or not path: | ||
return | ||
try: | ||
mtime = Path(path).stat().st_mtime | ||
diff = self._last_mtime < mtime | ||
if diff > 0: | ||
self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path) | ||
self.open() | ||
except OSError as exc: | ||
self.logger.warning("Failed to check MMDB age", exc=exc) | ||
|
||
@property | ||
def enabled(self) -> bool: | ||
"""Check if MMDB is enabled""" | ||
return bool(self.reader) |
Oops, something went wrong.