diff --git a/Dockerfile b/Dockerfile index 629d3258b..114be6253 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,7 +71,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ # Stage 4: MaxMind GeoIP FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v6.0 as geoip -ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City" +ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN" ENV GEOIPUPDATE_VERBOSE="true" ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID" ENV GEOIPUPDATE_LICENSE_KEY_FILE="/run/secrets/GEOIPUPDATE_LICENSE_KEY" diff --git a/authentik/api/v3/config.py b/authentik/api/v3/config.py index 0defd1a5b..93b783629 100644 --- a/authentik/api/v3/config.py +++ b/authentik/api/v3/config.py @@ -19,7 +19,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from authentik.core.api.utils import PassiveSerializer -from authentik.events.geo import GEOIP_READER +from authentik.events.context_processors.base import get_context_processors from authentik.lib.config import CONFIG capabilities = Signal() @@ -30,6 +30,7 @@ class Capabilities(models.TextChoices): CAN_SAVE_MEDIA = "can_save_media" CAN_GEO_IP = "can_geo_ip" + CAN_ASN = "can_asn" CAN_IMPERSONATE = "can_impersonate" CAN_DEBUG = "can_debug" IS_ENTERPRISE = "is_enterprise" @@ -68,8 +69,9 @@ class ConfigView(APIView): deb_test = settings.DEBUG or settings.TEST if Path(settings.MEDIA_ROOT).is_mount() or deb_test: caps.append(Capabilities.CAN_SAVE_MEDIA) - if GEOIP_READER.enabled: - caps.append(Capabilities.CAN_GEO_IP) + for processor in get_context_processors(): + if cap := processor.capability(): + caps.append(cap) if CONFIG.get_bool("impersonation"): caps.append(Capabilities.CAN_IMPERSONATE) if settings.DEBUG: # pragma: no cover diff --git a/authentik/core/api/authenticated_sessions.py b/authentik/core/api/authenticated_sessions.py index 03c1aeaf3..2d77937be 100644 --- a/authentik/core/api/authenticated_sessions.py +++ b/authentik/core/api/authenticated_sessions.py @@ -14,7 +14,8 @@ from ua_parser import user_agent_parser from authentik.api.authorization import OwnerSuperuserPermissions from authentik.core.api.used_by import UsedByMixin from authentik.core.models import AuthenticatedSession -from authentik.events.geo import GEOIP_READER, GeoIPDict +from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR, ASNDict +from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR, GeoIPDict class UserAgentDeviceDict(TypedDict): @@ -59,6 +60,7 @@ class AuthenticatedSessionSerializer(ModelSerializer): current = SerializerMethodField() user_agent = SerializerMethodField() geo_ip = SerializerMethodField() + asn = SerializerMethodField() def get_current(self, instance: AuthenticatedSession) -> bool: """Check if session is currently active session""" @@ -70,8 +72,12 @@ class AuthenticatedSessionSerializer(ModelSerializer): return user_agent_parser.Parse(instance.last_user_agent) def get_geo_ip(self, instance: AuthenticatedSession) -> Optional[GeoIPDict]: # pragma: no cover - """Get parsed user agent""" - return GEOIP_READER.city_dict(instance.last_ip) + """Get GeoIP Data""" + return GEOIP_CONTEXT_PROCESSOR.city_dict(instance.last_ip) + + def get_asn(self, instance: AuthenticatedSession) -> Optional[ASNDict]: # pragma: no cover + """Get ASN Data""" + return ASN_CONTEXT_PROCESSOR.asn_dict(instance.last_ip) class Meta: model = AuthenticatedSession @@ -80,6 +86,7 @@ class AuthenticatedSessionSerializer(ModelSerializer): "current", "user_agent", "geo_ip", + "asn", "user", "last_ip", "last_user_agent", diff --git a/authentik/events/context_processors/__init__.py b/authentik/events/context_processors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/events/context_processors/asn.py b/authentik/events/context_processors/asn.py new file mode 100644 index 000000000..afefbcbc6 --- /dev/null +++ b/authentik/events/context_processors/asn.py @@ -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() diff --git a/authentik/events/context_processors/base.py b/authentik/events/context_processors/base.py new file mode 100644 index 000000000..96a46a65a --- /dev/null +++ b/authentik/events/context_processors/base.py @@ -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 diff --git a/authentik/events/context_processors/geoip.py b/authentik/events/context_processors/geoip.py new file mode 100644 index 000000000..ca00a4c54 --- /dev/null +++ b/authentik/events/context_processors/geoip.py @@ -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() diff --git a/authentik/events/context_processors/mmdb.py b/authentik/events/context_processors/mmdb.py new file mode 100644 index 000000000..09c17f91f --- /dev/null +++ b/authentik/events/context_processors/mmdb.py @@ -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) diff --git a/authentik/events/geo.py b/authentik/events/geo.py deleted file mode 100644 index 95a28539c..000000000 --- a/authentik/events/geo.py +++ /dev/null @@ -1,100 +0,0 @@ -"""events GeoIP Reader""" -from os import stat -from typing import Optional, TypedDict - -from geoip2.database import Reader -from geoip2.errors import GeoIP2Error -from geoip2.models import City -from sentry_sdk.hub import Hub -from structlog.stdlib import get_logger - -from authentik.lib.config import CONFIG - -LOGGER = get_logger() - - -class GeoIPDict(TypedDict): - """GeoIP Details""" - - continent: str - country: str - lat: float - long: float - city: str - - -class GeoIPReader: - """Slim wrapper around GeoIP API""" - - def __init__(self): - self.__reader: Optional[Reader] = None - self.__last_mtime: float = 0.0 - self.__open() - - def __open(self): - """Get GeoIP Reader, if configured, otherwise none""" - path = CONFIG.get("geoip") - if path == "" or not path: - return - try: - self.__reader = Reader(path) - self.__last_mtime = stat(path).st_mtime - LOGGER.info("Loaded GeoIP database", last_write=self.__last_mtime) - except OSError as exc: - LOGGER.warning("Failed to load GeoIP database", exc=exc) - - def __check_expired(self): - """Check if the modification date of the GeoIP database has - changed, and reload it if so""" - path = CONFIG.get("geoip") - try: - mtime = stat(path).st_mtime - diff = self.__last_mtime < mtime - if diff > 0: - LOGGER.info("Found new GeoIP Database, reopening", diff=diff) - self.__open() - except OSError as exc: - LOGGER.warning("Failed to check GeoIP age", exc=exc) - return - - @property - def enabled(self) -> bool: - """Check if GeoIP is enabled""" - return bool(self.__reader) - - 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_READER = GeoIPReader() diff --git a/authentik/events/models.py b/authentik/events/models.py index 03e9a939c..965e38cc2 100644 --- a/authentik/events/models.py +++ b/authentik/events/models.py @@ -26,7 +26,7 @@ from authentik.core.middleware import ( SESSION_KEY_IMPERSONATE_USER, ) from authentik.core.models import ExpiringModel, Group, PropertyMapping, User -from authentik.events.geo import GEOIP_READER +from authentik.events.context_processors.base import get_context_processors from authentik.events.utils import ( cleanse_dict, get_user, @@ -246,21 +246,15 @@ class Event(SerializerModel, ExpiringModel): self.user["on_behalf_of"] = get_user(request.session[SESSION_KEY_IMPERSONATE_USER]) # User 255.255.255.255 as fallback if IP cannot be determined self.client_ip = ClientIPMiddleware.get_client_ip(request) - # Apply GeoIP Data, when enabled - self.with_geoip() + # Enrich event data + for processor in get_context_processors(): + processor.enrich_event(self) # If there's no app set, we get it from the requests too if not self.app: self.app = Event._get_app_from_request(request) self.save() return self - def with_geoip(self): # pragma: no cover - """Apply GeoIP Data, when enabled""" - city = GEOIP_READER.city_dict(self.client_ip) - if not city: - return - self.context["geo"] = city - def save(self, *args, **kwargs): if self._state.adding: LOGGER.info( diff --git a/authentik/events/tests/test_enrich_asn.py b/authentik/events/tests/test_enrich_asn.py new file mode 100644 index 000000000..2844c4591 --- /dev/null +++ b/authentik/events/tests/test_enrich_asn.py @@ -0,0 +1,24 @@ +"""Test ASN Wrapper""" +from django.test import TestCase + +from authentik.events.context_processors.asn import ASNContextProcessor + + +class TestASN(TestCase): + """Test ASN Wrapper""" + + def setUp(self) -> None: + self.reader = ASNContextProcessor() + + def test_simple(self): + """Test simple asn wrapper""" + # IPs from + # https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-ASN-Test.json + self.assertEqual( + self.reader.asn_dict("1.0.0.1"), + { + "asn": 15169, + "as_org": "Google Inc.", + "network": "1.0.0.0/24", + }, + ) diff --git a/authentik/events/tests/test_geoip.py b/authentik/events/tests/test_enrich_geoip.py similarity index 83% rename from authentik/events/tests/test_geoip.py rename to authentik/events/tests/test_enrich_geoip.py index 3120dacae..5317901a4 100644 --- a/authentik/events/tests/test_geoip.py +++ b/authentik/events/tests/test_enrich_geoip.py @@ -1,14 +1,14 @@ """Test GeoIP Wrapper""" from django.test import TestCase -from authentik.events.geo import GeoIPReader +from authentik.events.context_processors.geoip import GeoIPContextProcessor class TestGeoIP(TestCase): """Test GeoIP Wrapper""" def setUp(self) -> None: - self.reader = GeoIPReader() + self.reader = GeoIPContextProcessor() def test_simple(self): """Test simple city wrapper""" diff --git a/authentik/events/utils.py b/authentik/events/utils.py index a7c3bdf3e..cf1b1b78b 100644 --- a/authentik/events/utils.py +++ b/authentik/events/utils.py @@ -17,12 +17,13 @@ from django.db.models.base import Model from django.http.request import HttpRequest from django.utils import timezone from django.views.debug import SafeExceptionReporterFilter -from geoip2.models import City +from geoip2.models import ASN, City from guardian.utils import get_anonymous_user from authentik.blueprints.v1.common import YAMLTag from authentik.core.models import User -from authentik.events.geo import GEOIP_READER +from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR +from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR from authentik.policies.types import PolicyRequest # Special keys which are *not* cleaned, even when the default filter @@ -123,7 +124,9 @@ def sanitize_item(value: Any) -> Any: if isinstance(value, (HttpRequest, WSGIRequest)): return ... if isinstance(value, City): - return GEOIP_READER.city_to_dict(value) + return GEOIP_CONTEXT_PROCESSOR.city_to_dict(value) + if isinstance(value, ASN): + return ASN_CONTEXT_PROCESSOR.asn_to_dict(value) if isinstance(value, Path): return str(value) if isinstance(value, Exception): diff --git a/authentik/lib/config.py b/authentik/lib/config.py index dd5500f8e..3b977e8a2 100644 --- a/authentik/lib/config.py +++ b/authentik/lib/config.py @@ -35,6 +35,7 @@ REDIS_ENV_KEYS = [ ] DEPRECATIONS = { + "geoip": "events.context_processors.geoip", "redis.broker_url": "broker.url", "redis.broker_transport_options": "broker.transport_options", "redis.cache_timeout": "cache.timeout", diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index 7573888ae..afb775c44 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -108,7 +108,10 @@ cookie_domain: null disable_update_check: false disable_startup_analytics: false avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar,initials -geoip: "/geoip/GeoLite2-City.mmdb" +events: + context_processors: + geoip: "/geoip/GeoLite2-City.mmdb" + asn: "/geoip/GeoLite2-ASN.mmdb" footer_links: [] diff --git a/authentik/policies/reputation/api.py b/authentik/policies/reputation/api.py index 9e9d95e13..885f6c162 100644 --- a/authentik/policies/reputation/api.py +++ b/authentik/policies/reputation/api.py @@ -47,6 +47,7 @@ class ReputationSerializer(ModelSerializer): "identifier", "ip", "ip_geo_data", + "ip_asn_data", "score", "updated", ] diff --git a/authentik/policies/reputation/migrations/0006_reputation_ip_asn_data.py b/authentik/policies/reputation/migrations/0006_reputation_ip_asn_data.py new file mode 100644 index 000000000..05557392e --- /dev/null +++ b/authentik/policies/reputation/migrations/0006_reputation_ip_asn_data.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.7 on 2023-12-05 22:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("authentik_policies_reputation", "0005_reputation_expires_reputation_expiring"), + ] + + operations = [ + migrations.AddField( + model_name="reputation", + name="ip_asn_data", + field=models.JSONField(default=dict), + ), + ] diff --git a/authentik/policies/reputation/models.py b/authentik/policies/reputation/models.py index 7fccfa11a..ea8ac2bd6 100644 --- a/authentik/policies/reputation/models.py +++ b/authentik/policies/reputation/models.py @@ -76,6 +76,7 @@ class Reputation(ExpiringModel, SerializerModel): identifier = models.TextField() ip = models.GenericIPAddressField() ip_geo_data = models.JSONField(default=dict) + ip_asn_data = models.JSONField(default=dict) score = models.BigIntegerField(default=0) expires = models.DateTimeField(default=reputation_expiry) diff --git a/authentik/policies/reputation/tasks.py b/authentik/policies/reputation/tasks.py index 7fd7b775f..ac65d1748 100644 --- a/authentik/policies/reputation/tasks.py +++ b/authentik/policies/reputation/tasks.py @@ -2,7 +2,8 @@ from django.core.cache import cache from structlog.stdlib import get_logger -from authentik.events.geo import GEOIP_READER +from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR +from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR from authentik.events.monitored_tasks import ( MonitoredTask, TaskResult, @@ -26,7 +27,8 @@ def save_reputation(self: MonitoredTask): ip=score["ip"], identifier=score["identifier"], ) - rep.ip_geo_data = GEOIP_READER.city_dict(score["ip"]) or {} + rep.ip_geo_data = GEOIP_CONTEXT_PROCESSOR.city_dict(score["ip"]) or {} + rep.ip_asn_data = ASN_CONTEXT_PROCESSOR.asn_dict(score["ip"]) or {} rep.score = score["score"] objects_to_update.append(rep) Reputation.objects.bulk_update(objects_to_update, ["score", "ip_geo_data"]) diff --git a/authentik/policies/types.py b/authentik/policies/types.py index 5e59dbbf0..c9e2e8d86 100644 --- a/authentik/policies/types.py +++ b/authentik/policies/types.py @@ -8,7 +8,7 @@ from django.db.models import Model from django.http import HttpRequest from structlog.stdlib import get_logger -from authentik.events.geo import GEOIP_READER +from authentik.events.context_processors.base import get_context_processors if TYPE_CHECKING: from authentik.core.models import User @@ -37,15 +37,9 @@ class PolicyRequest: def set_http_request(self, request: HttpRequest): # pragma: no cover """Load data from HTTP request, including geoip when enabled""" - from authentik.root.middleware import ClientIPMiddleware - self.http_request = request - if not GEOIP_READER.enabled: - return - client_ip = ClientIPMiddleware.get_client_ip(request) - if not client_ip: - return - self.context["geoip"] = GEOIP_READER.city(client_ip) + for processor in get_context_processors(): + self.context.update(processor.enrich_context(request)) @property def should_cache(self) -> bool: diff --git a/authentik/root/test_runner.py b/authentik/root/test_runner.py index b2bf7a3d7..bc3b3b968 100644 --- a/authentik/root/test_runner.py +++ b/authentik/root/test_runner.py @@ -32,7 +32,8 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover settings.TEST = True settings.CELERY["task_always_eager"] = True CONFIG.set("avatars", "none") - CONFIG.set("geoip", "tests/GeoLite2-City-Test.mmdb") + CONFIG.set("events.context_processors.geoip", "tests/GeoLite2-City-Test.mmdb") + CONFIG.set("events.context_processors.asn", "tests/GeoLite2-ASN-Test.mmdb") CONFIG.set("blueprints_dir", "./blueprints") CONFIG.set( "outposts.container_image_base", diff --git a/blueprints/schema.json b/blueprints/schema.json index bf66c94ed..eaaed0a8d 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -3756,6 +3756,11 @@ "additionalProperties": true, "title": "Ip geo data" }, + "ip_asn_data": { + "type": "object", + "additionalProperties": true, + "title": "Ip asn data" + }, "score": { "type": "integer", "minimum": -9223372036854775808, diff --git a/schema.yml b/schema.yml index da1a90a18..fee6b8d3d 100644 --- a/schema.yml +++ b/schema.yml @@ -28280,7 +28280,7 @@ components: readOnly: true geo_ip: type: object - description: Get parsed user agent + description: Get GeoIP Data properties: continent: type: string @@ -28302,6 +28302,24 @@ components: - long nullable: true readOnly: true + asn: + type: object + description: Get ASN Data + properties: + asn: + type: integer + as_org: + type: string + nullable: true + network: + type: string + nullable: true + required: + - as_org + - asn + - network + nullable: true + readOnly: true user: type: integer last_ip: @@ -28316,6 +28334,7 @@ components: type: string format: date-time required: + - asn - current - geo_ip - last_ip @@ -29283,6 +29302,7 @@ components: enum: - can_save_media - can_geo_ip + - can_asn - can_impersonate - can_debug - is_enterprise @@ -29290,6 +29310,7 @@ components: description: |- * `can_save_media` - Can Save Media * `can_geo_ip` - Can Geo Ip + * `can_asn` - Can Asn * `can_impersonate` - Can Impersonate * `can_debug` - Can Debug * `is_enterprise` - Is Enterprise @@ -39667,6 +39688,7 @@ components: ip: type: string ip_geo_data: {} + ip_asn_data: {} score: type: integer maximum: 9223372036854775807 diff --git a/scripts/generate_config.py b/scripts/generate_config.py index 187eb3ba5..965e3e15e 100644 --- a/scripts/generate_config.py +++ b/scripts/generate_config.py @@ -17,7 +17,12 @@ with open("local.env.yml", "w", encoding="utf-8") as _config: }, "blueprints_dir": "./blueprints", "cert_discovery_dir": "./certs", - "geoip": "tests/GeoLite2-City-Test.mmdb", + "events": { + "processors": { + "geoip": "tests/GeoLite2-City-Test.mmdb", + "asn": "tests/GeoLite2-ASN-Test.mmdb", + } + }, }, _config, default_flow_style=False, diff --git a/tests/GeoLite2-ASN-Test.mmdb b/tests/GeoLite2-ASN-Test.mmdb new file mode 100644 index 000000000..3e5033144 Binary files /dev/null and b/tests/GeoLite2-ASN-Test.mmdb differ diff --git a/website/docs/core/geoip.mdx b/website/docs/core/geoip.mdx index b3cedbe47..ff73e7a39 100644 --- a/website/docs/core/geoip.mdx +++ b/website/docs/core/geoip.mdx @@ -27,7 +27,8 @@ import TabItem from "@theme/TabItem"; Add the following block to your `.env` file: ```shell -AUTHENTIK_GEOIP=/tmp/non-existent-file +AUTHENTIK_EVENTS__CONTEXT_PROCESSORS__GEOIP=/tmp/non-existent-file +AUTHENTIK_EVENTS__CONTEXT_PROCESSORS__ASN=/tmp/non-existent-file ``` Afterwards, run the upgrade commands from the latest release notes. @@ -38,7 +39,10 @@ Add the following block to your `values.yml` file: ```yaml authentik: - geoip: /tmp/non-existent-file + events: + context_processors: + geoip: "/tmp/non-existent-file" + asn: "/tmp/non-existent-file" ``` Afterwards, run the upgrade commands from the latest release notes. @@ -74,7 +78,7 @@ services: volumes: - "geoip:/usr/share/GeoIP" environment: - GEOIPUPDATE_EDITION_IDS: "GeoLite2-City" + GEOIPUPDATE_EDITION_IDS: "GeoLite2-City GeoLite2-ASN" GEOIPUPDATE_FREQUENCY: "8" GEOIPUPDATE_ACCOUNT_ID: "*your account ID*" GEOIPUPDATE_LICENSE_KEY: "*your license key*" @@ -94,7 +98,7 @@ geoip: enabled: true accountId: "*your account ID*" licenseKey: "*your license key*" - editionIds: "GeoLite2-City" + editionIds: "GeoLite2-City GeoLite2-ASN" image: maxmindinc/geoipupdate:v4.8 updateInterval: 8 ``` diff --git a/website/docs/installation/configuration.mdx b/website/docs/installation/configuration.mdx index 28d8e5f50..6201467f1 100644 --- a/website/docs/installation/configuration.mdx +++ b/website/docs/installation/configuration.mdx @@ -154,9 +154,13 @@ Defaults to `info`. Which domain the session cookie should be set to. By default, the cookie is set to the domain authentik is accessed under. -### `AUTHENTIK_GEOIP` +### `AUTHENTIK_EVENTS__CONTEXT_PROCESSORS__GEOIP` -Path to the GeoIP database. Defaults to `/geoip/GeoLite2-City.mmdb`. If the file is not found, authentik will skip GeoIP support. +Path to the GeoIP City database. Defaults to `/geoip/GeoLite2-City.mmdb`. If the file is not found, authentik will skip GeoIP support. + +### `AUTHENTIK_EVENTS__CONTEXT_PROCESSORS__ASN` + +Path to the GeoIP ASN database. Defaults to `/geoip/GeoLite2-ASN.mmdb`. If the file is not found, authentik will skip GeoIP support. ### `AUTHENTIK_DISABLE_UPDATE_CHECK`