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>
This commit is contained in:
Jens L 2023-12-20 22:16:50 +01:00 committed by GitHub
parent 4ff3915d59
commit 50860d7ffe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 393 additions and 143 deletions

View File

@ -71,7 +71,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
# Stage 4: MaxMind GeoIP # Stage 4: MaxMind GeoIP
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v6.0 as 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_VERBOSE="true"
ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID" ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID"
ENV GEOIPUPDATE_LICENSE_KEY_FILE="/run/secrets/GEOIPUPDATE_LICENSE_KEY" ENV GEOIPUPDATE_LICENSE_KEY_FILE="/run/secrets/GEOIPUPDATE_LICENSE_KEY"

View File

@ -19,7 +19,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer 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 from authentik.lib.config import CONFIG
capabilities = Signal() capabilities = Signal()
@ -30,6 +30,7 @@ class Capabilities(models.TextChoices):
CAN_SAVE_MEDIA = "can_save_media" CAN_SAVE_MEDIA = "can_save_media"
CAN_GEO_IP = "can_geo_ip" CAN_GEO_IP = "can_geo_ip"
CAN_ASN = "can_asn"
CAN_IMPERSONATE = "can_impersonate" CAN_IMPERSONATE = "can_impersonate"
CAN_DEBUG = "can_debug" CAN_DEBUG = "can_debug"
IS_ENTERPRISE = "is_enterprise" IS_ENTERPRISE = "is_enterprise"
@ -68,8 +69,9 @@ class ConfigView(APIView):
deb_test = settings.DEBUG or settings.TEST deb_test = settings.DEBUG or settings.TEST
if Path(settings.MEDIA_ROOT).is_mount() or deb_test: if Path(settings.MEDIA_ROOT).is_mount() or deb_test:
caps.append(Capabilities.CAN_SAVE_MEDIA) caps.append(Capabilities.CAN_SAVE_MEDIA)
if GEOIP_READER.enabled: for processor in get_context_processors():
caps.append(Capabilities.CAN_GEO_IP) if cap := processor.capability():
caps.append(cap)
if CONFIG.get_bool("impersonation"): if CONFIG.get_bool("impersonation"):
caps.append(Capabilities.CAN_IMPERSONATE) caps.append(Capabilities.CAN_IMPERSONATE)
if settings.DEBUG: # pragma: no cover if settings.DEBUG: # pragma: no cover

View File

@ -14,7 +14,8 @@ from ua_parser import user_agent_parser
from authentik.api.authorization import OwnerSuperuserPermissions from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import AuthenticatedSession 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): class UserAgentDeviceDict(TypedDict):
@ -59,6 +60,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
current = SerializerMethodField() current = SerializerMethodField()
user_agent = SerializerMethodField() user_agent = SerializerMethodField()
geo_ip = SerializerMethodField() geo_ip = SerializerMethodField()
asn = SerializerMethodField()
def get_current(self, instance: AuthenticatedSession) -> bool: def get_current(self, instance: AuthenticatedSession) -> bool:
"""Check if session is currently active session""" """Check if session is currently active session"""
@ -70,8 +72,12 @@ class AuthenticatedSessionSerializer(ModelSerializer):
return user_agent_parser.Parse(instance.last_user_agent) return user_agent_parser.Parse(instance.last_user_agent)
def get_geo_ip(self, instance: AuthenticatedSession) -> Optional[GeoIPDict]: # pragma: no cover def get_geo_ip(self, instance: AuthenticatedSession) -> Optional[GeoIPDict]: # pragma: no cover
"""Get parsed user agent""" """Get GeoIP Data"""
return GEOIP_READER.city_dict(instance.last_ip) 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: class Meta:
model = AuthenticatedSession model = AuthenticatedSession
@ -80,6 +86,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
"current", "current",
"user_agent", "user_agent",
"geo_ip", "geo_ip",
"asn",
"user", "user",
"last_ip", "last_ip",
"last_user_agent", "last_user_agent",

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -26,7 +26,7 @@ from authentik.core.middleware import (
SESSION_KEY_IMPERSONATE_USER, SESSION_KEY_IMPERSONATE_USER,
) )
from authentik.core.models import ExpiringModel, Group, PropertyMapping, 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 ( from authentik.events.utils import (
cleanse_dict, cleanse_dict,
get_user, get_user,
@ -246,21 +246,15 @@ class Event(SerializerModel, ExpiringModel):
self.user["on_behalf_of"] = get_user(request.session[SESSION_KEY_IMPERSONATE_USER]) 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 # User 255.255.255.255 as fallback if IP cannot be determined
self.client_ip = ClientIPMiddleware.get_client_ip(request) self.client_ip = ClientIPMiddleware.get_client_ip(request)
# Apply GeoIP Data, when enabled # Enrich event data
self.with_geoip() for processor in get_context_processors():
processor.enrich_event(self)
# If there's no app set, we get it from the requests too # If there's no app set, we get it from the requests too
if not self.app: if not self.app:
self.app = Event._get_app_from_request(request) self.app = Event._get_app_from_request(request)
self.save() self.save()
return self 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): def save(self, *args, **kwargs):
if self._state.adding: if self._state.adding:
LOGGER.info( LOGGER.info(

View File

@ -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",
},
)

View File

@ -1,14 +1,14 @@
"""Test GeoIP Wrapper""" """Test GeoIP Wrapper"""
from django.test import TestCase from django.test import TestCase
from authentik.events.geo import GeoIPReader from authentik.events.context_processors.geoip import GeoIPContextProcessor
class TestGeoIP(TestCase): class TestGeoIP(TestCase):
"""Test GeoIP Wrapper""" """Test GeoIP Wrapper"""
def setUp(self) -> None: def setUp(self) -> None:
self.reader = GeoIPReader() self.reader = GeoIPContextProcessor()
def test_simple(self): def test_simple(self):
"""Test simple city wrapper""" """Test simple city wrapper"""

View File

@ -17,12 +17,13 @@ from django.db.models.base import Model
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.utils import timezone from django.utils import timezone
from django.views.debug import SafeExceptionReporterFilter 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 guardian.utils import get_anonymous_user
from authentik.blueprints.v1.common import YAMLTag from authentik.blueprints.v1.common import YAMLTag
from authentik.core.models import User 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 from authentik.policies.types import PolicyRequest
# Special keys which are *not* cleaned, even when the default filter # 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)): if isinstance(value, (HttpRequest, WSGIRequest)):
return ... return ...
if isinstance(value, City): 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): if isinstance(value, Path):
return str(value) return str(value)
if isinstance(value, Exception): if isinstance(value, Exception):

View File

@ -35,6 +35,7 @@ REDIS_ENV_KEYS = [
] ]
DEPRECATIONS = { DEPRECATIONS = {
"geoip": "events.context_processors.geoip",
"redis.broker_url": "broker.url", "redis.broker_url": "broker.url",
"redis.broker_transport_options": "broker.transport_options", "redis.broker_transport_options": "broker.transport_options",
"redis.cache_timeout": "cache.timeout", "redis.cache_timeout": "cache.timeout",

View File

@ -108,7 +108,10 @@ cookie_domain: null
disable_update_check: false disable_update_check: false
disable_startup_analytics: false disable_startup_analytics: false
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar,initials 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: [] footer_links: []

View File

@ -47,6 +47,7 @@ class ReputationSerializer(ModelSerializer):
"identifier", "identifier",
"ip", "ip",
"ip_geo_data", "ip_geo_data",
"ip_asn_data",
"score", "score",
"updated", "updated",
] ]

View File

@ -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),
),
]

View File

@ -76,6 +76,7 @@ class Reputation(ExpiringModel, SerializerModel):
identifier = models.TextField() identifier = models.TextField()
ip = models.GenericIPAddressField() ip = models.GenericIPAddressField()
ip_geo_data = models.JSONField(default=dict) ip_geo_data = models.JSONField(default=dict)
ip_asn_data = models.JSONField(default=dict)
score = models.BigIntegerField(default=0) score = models.BigIntegerField(default=0)
expires = models.DateTimeField(default=reputation_expiry) expires = models.DateTimeField(default=reputation_expiry)

View File

@ -2,7 +2,8 @@
from django.core.cache import cache from django.core.cache import cache
from structlog.stdlib import get_logger 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 ( from authentik.events.monitored_tasks import (
MonitoredTask, MonitoredTask,
TaskResult, TaskResult,
@ -26,7 +27,8 @@ def save_reputation(self: MonitoredTask):
ip=score["ip"], ip=score["ip"],
identifier=score["identifier"], 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"] rep.score = score["score"]
objects_to_update.append(rep) objects_to_update.append(rep)
Reputation.objects.bulk_update(objects_to_update, ["score", "ip_geo_data"]) Reputation.objects.bulk_update(objects_to_update, ["score", "ip_geo_data"])

View File

@ -8,7 +8,7 @@ from django.db.models import Model
from django.http import HttpRequest from django.http import HttpRequest
from structlog.stdlib import get_logger 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: if TYPE_CHECKING:
from authentik.core.models import User from authentik.core.models import User
@ -37,15 +37,9 @@ class PolicyRequest:
def set_http_request(self, request: HttpRequest): # pragma: no cover def set_http_request(self, request: HttpRequest): # pragma: no cover
"""Load data from HTTP request, including geoip when enabled""" """Load data from HTTP request, including geoip when enabled"""
from authentik.root.middleware import ClientIPMiddleware
self.http_request = request self.http_request = request
if not GEOIP_READER.enabled: for processor in get_context_processors():
return self.context.update(processor.enrich_context(request))
client_ip = ClientIPMiddleware.get_client_ip(request)
if not client_ip:
return
self.context["geoip"] = GEOIP_READER.city(client_ip)
@property @property
def should_cache(self) -> bool: def should_cache(self) -> bool:

View File

@ -32,7 +32,8 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
settings.TEST = True settings.TEST = True
settings.CELERY["task_always_eager"] = True settings.CELERY["task_always_eager"] = True
CONFIG.set("avatars", "none") 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("blueprints_dir", "./blueprints")
CONFIG.set( CONFIG.set(
"outposts.container_image_base", "outposts.container_image_base",

View File

@ -3756,6 +3756,11 @@
"additionalProperties": true, "additionalProperties": true,
"title": "Ip geo data" "title": "Ip geo data"
}, },
"ip_asn_data": {
"type": "object",
"additionalProperties": true,
"title": "Ip asn data"
},
"score": { "score": {
"type": "integer", "type": "integer",
"minimum": -9223372036854775808, "minimum": -9223372036854775808,

View File

@ -28280,7 +28280,7 @@ components:
readOnly: true readOnly: true
geo_ip: geo_ip:
type: object type: object
description: Get parsed user agent description: Get GeoIP Data
properties: properties:
continent: continent:
type: string type: string
@ -28302,6 +28302,24 @@ components:
- long - long
nullable: true nullable: true
readOnly: 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: user:
type: integer type: integer
last_ip: last_ip:
@ -28316,6 +28334,7 @@ components:
type: string type: string
format: date-time format: date-time
required: required:
- asn
- current - current
- geo_ip - geo_ip
- last_ip - last_ip
@ -29283,6 +29302,7 @@ components:
enum: enum:
- can_save_media - can_save_media
- can_geo_ip - can_geo_ip
- can_asn
- can_impersonate - can_impersonate
- can_debug - can_debug
- is_enterprise - is_enterprise
@ -29290,6 +29310,7 @@ components:
description: |- description: |-
* `can_save_media` - Can Save Media * `can_save_media` - Can Save Media
* `can_geo_ip` - Can Geo Ip * `can_geo_ip` - Can Geo Ip
* `can_asn` - Can Asn
* `can_impersonate` - Can Impersonate * `can_impersonate` - Can Impersonate
* `can_debug` - Can Debug * `can_debug` - Can Debug
* `is_enterprise` - Is Enterprise * `is_enterprise` - Is Enterprise
@ -39667,6 +39688,7 @@ components:
ip: ip:
type: string type: string
ip_geo_data: {} ip_geo_data: {}
ip_asn_data: {}
score: score:
type: integer type: integer
maximum: 9223372036854775807 maximum: 9223372036854775807

View File

@ -17,7 +17,12 @@ with open("local.env.yml", "w", encoding="utf-8") as _config:
}, },
"blueprints_dir": "./blueprints", "blueprints_dir": "./blueprints",
"cert_discovery_dir": "./certs", "cert_discovery_dir": "./certs",
"events": {
"processors": {
"geoip": "tests/GeoLite2-City-Test.mmdb", "geoip": "tests/GeoLite2-City-Test.mmdb",
"asn": "tests/GeoLite2-ASN-Test.mmdb",
}
},
}, },
_config, _config,
default_flow_style=False, default_flow_style=False,

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -27,7 +27,8 @@ import TabItem from "@theme/TabItem";
Add the following block to your `.env` file: Add the following block to your `.env` file:
```shell ```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. Afterwards, run the upgrade commands from the latest release notes.
@ -38,7 +39,10 @@ Add the following block to your `values.yml` file:
```yaml ```yaml
authentik: 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. Afterwards, run the upgrade commands from the latest release notes.
@ -74,7 +78,7 @@ services:
volumes: volumes:
- "geoip:/usr/share/GeoIP" - "geoip:/usr/share/GeoIP"
environment: environment:
GEOIPUPDATE_EDITION_IDS: "GeoLite2-City" GEOIPUPDATE_EDITION_IDS: "GeoLite2-City GeoLite2-ASN"
GEOIPUPDATE_FREQUENCY: "8" GEOIPUPDATE_FREQUENCY: "8"
GEOIPUPDATE_ACCOUNT_ID: "*your account ID*" GEOIPUPDATE_ACCOUNT_ID: "*your account ID*"
GEOIPUPDATE_LICENSE_KEY: "*your license key*" GEOIPUPDATE_LICENSE_KEY: "*your license key*"
@ -94,7 +98,7 @@ geoip:
enabled: true enabled: true
accountId: "*your account ID*" accountId: "*your account ID*"
licenseKey: "*your license key*" licenseKey: "*your license key*"
editionIds: "GeoLite2-City" editionIds: "GeoLite2-City GeoLite2-ASN"
image: maxmindinc/geoipupdate:v4.8 image: maxmindinc/geoipupdate:v4.8
updateInterval: 8 updateInterval: 8
``` ```

View File

@ -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. 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` ### `AUTHENTIK_DISABLE_UPDATE_CHECK`