events: add context manager to ignore/modify audit events being written (#9181)
This commit is contained in:
@ -1,6 +1,8 @@
|
|||||||
"""Events middleware"""
|
"""Events middleware"""
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from contextvars import ContextVar
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -31,6 +33,9 @@ IGNORED_MODELS = tuple(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None)
|
||||||
|
_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False)
|
||||||
|
|
||||||
|
|
||||||
def should_log_model(model: Model) -> bool:
|
def should_log_model(model: Model) -> bool:
|
||||||
"""Return true if operation on `model` should be logged"""
|
"""Return true if operation on `model` should be logged"""
|
||||||
@ -44,6 +49,28 @@ def should_log_m2m(model: Model) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def audit_overwrite_user(user: User):
|
||||||
|
"""Overwrite user being logged for model AuditMiddleware. Commonly used
|
||||||
|
for example in flows where a pending user is given, but the request is not authenticated yet"""
|
||||||
|
_CTX_OVERWRITE_USER.set(user)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
_CTX_OVERWRITE_USER.set(None)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def audit_ignore():
|
||||||
|
"""Ignore model operations in the block. Useful for objects which need to be modified
|
||||||
|
but are not excluded (e.g. WebAuthn devices)"""
|
||||||
|
_CTX_IGNORE.set(True)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
_CTX_IGNORE.set(False)
|
||||||
|
|
||||||
|
|
||||||
class EventNewThread(Thread):
|
class EventNewThread(Thread):
|
||||||
"""Create Event in background thread"""
|
"""Create Event in background thread"""
|
||||||
|
|
||||||
@ -158,6 +185,10 @@ class AuditMiddleware:
|
|||||||
"""Signal handler for all object's post_save"""
|
"""Signal handler for all object's post_save"""
|
||||||
if not should_log_model(instance):
|
if not should_log_model(instance):
|
||||||
return
|
return
|
||||||
|
if _CTX_IGNORE.get():
|
||||||
|
return
|
||||||
|
if _new_user := _CTX_OVERWRITE_USER.get():
|
||||||
|
user = _new_user
|
||||||
|
|
||||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||||
thread = EventNewThread(action, request, user=user, model=model_to_dict(instance))
|
thread = EventNewThread(action, request, user=user, model=model_to_dict(instance))
|
||||||
@ -168,6 +199,10 @@ class AuditMiddleware:
|
|||||||
"""Signal handler for all object's pre_delete"""
|
"""Signal handler for all object's pre_delete"""
|
||||||
if not should_log_model(instance): # pragma: no cover
|
if not should_log_model(instance): # pragma: no cover
|
||||||
return
|
return
|
||||||
|
if _CTX_IGNORE.get():
|
||||||
|
return
|
||||||
|
if _new_user := _CTX_OVERWRITE_USER.get():
|
||||||
|
user = _new_user
|
||||||
|
|
||||||
EventNewThread(
|
EventNewThread(
|
||||||
EventAction.MODEL_DELETED,
|
EventAction.MODEL_DELETED,
|
||||||
@ -184,6 +219,10 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
if not should_log_m2m(instance):
|
if not should_log_m2m(instance):
|
||||||
return
|
return
|
||||||
|
if _CTX_IGNORE.get():
|
||||||
|
return
|
||||||
|
if _new_user := _CTX_OVERWRITE_USER.get():
|
||||||
|
user = _new_user
|
||||||
|
|
||||||
EventNewThread(
|
EventNewThread(
|
||||||
EventAction.MODEL_UPDATED,
|
EventAction.MODEL_UPDATED,
|
||||||
|
|||||||
@ -5,7 +5,9 @@ from rest_framework.test import APITestCase
|
|||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
from authentik.events.middleware import audit_ignore, audit_overwrite_user
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
class TestEventsMiddleware(APITestCase):
|
class TestEventsMiddleware(APITestCase):
|
||||||
@ -15,35 +17,74 @@ class TestEventsMiddleware(APITestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = create_test_admin_user()
|
self.user = create_test_admin_user()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
Event.objects.all().delete()
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
"""Test model creation event"""
|
"""Test model creation event"""
|
||||||
|
uid = generate_id()
|
||||||
self.client.post(
|
self.client.post(
|
||||||
reverse("authentik_api:application-list"),
|
reverse("authentik_api:application-list"),
|
||||||
data={"name": "test-create", "slug": "test-create"},
|
data={"name": uid, "slug": uid},
|
||||||
)
|
)
|
||||||
self.assertTrue(Application.objects.filter(name="test-create").exists())
|
self.assertTrue(Application.objects.filter(name=uid).exists())
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
Event.objects.filter(
|
Event.objects.filter(
|
||||||
action=EventAction.MODEL_CREATED,
|
action=EventAction.MODEL_CREATED,
|
||||||
context__model__model_name="application",
|
context__model__model_name="application",
|
||||||
context__model__app="authentik_core",
|
context__model__app="authentik_core",
|
||||||
context__model__name="test-create",
|
context__model__name=uid,
|
||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
"""Test model creation event"""
|
"""Test model creation event"""
|
||||||
Application.objects.create(name="test-delete", slug="test-delete")
|
uid = generate_id()
|
||||||
self.client.delete(
|
Application.objects.create(name=uid, slug=uid)
|
||||||
reverse("authentik_api:application-detail", kwargs={"slug": "test-delete"})
|
self.client.delete(reverse("authentik_api:application-detail", kwargs={"slug": uid}))
|
||||||
)
|
|
||||||
self.assertFalse(Application.objects.filter(name="test").exists())
|
self.assertFalse(Application.objects.filter(name="test").exists())
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
Event.objects.filter(
|
Event.objects.filter(
|
||||||
action=EventAction.MODEL_DELETED,
|
action=EventAction.MODEL_DELETED,
|
||||||
context__model__model_name="application",
|
context__model__model_name="application",
|
||||||
context__model__app="authentik_core",
|
context__model__app="authentik_core",
|
||||||
context__model__name="test-delete",
|
context__model__name=uid,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_audit_ignore(self):
|
||||||
|
"""Test audit_ignore context manager"""
|
||||||
|
uid = generate_id()
|
||||||
|
with audit_ignore():
|
||||||
|
self.client.post(
|
||||||
|
reverse("authentik_api:application-list"),
|
||||||
|
data={"name": uid, "slug": uid},
|
||||||
|
)
|
||||||
|
self.assertTrue(Application.objects.filter(name=uid).exists())
|
||||||
|
self.assertFalse(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.MODEL_CREATED,
|
||||||
|
context__model__model_name="application",
|
||||||
|
context__model__app="authentik_core",
|
||||||
|
context__model__name=uid,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_audit_overwrite_user(self):
|
||||||
|
"""Test audit_overwrite_user context manager"""
|
||||||
|
uid = generate_id()
|
||||||
|
new_user = create_test_admin_user()
|
||||||
|
with audit_overwrite_user(new_user):
|
||||||
|
self.client.post(
|
||||||
|
reverse("authentik_api:application-list"),
|
||||||
|
data={"name": uid, "slug": uid},
|
||||||
|
)
|
||||||
|
self.assertTrue(Application.objects.filter(name=uid).exists())
|
||||||
|
self.assertTrue(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.MODEL_CREATED,
|
||||||
|
context__model__model_name="application",
|
||||||
|
context__model__app="authentik_core",
|
||||||
|
context__model__name=uid,
|
||||||
|
user__username=new_user.username,
|
||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
|
|||||||
@ -31,6 +31,7 @@ from authentik.core.models import (
|
|||||||
User,
|
User,
|
||||||
UserTypes,
|
UserTypes,
|
||||||
)
|
)
|
||||||
|
from authentik.events.middleware import audit_ignore
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.events.signals import get_login_event
|
from authentik.events.signals import get_login_event
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION
|
||||||
@ -465,22 +466,25 @@ class TokenParams:
|
|||||||
|
|
||||||
def __create_user_from_jwt(self, token: dict[str, Any], app: Application, source: OAuthSource):
|
def __create_user_from_jwt(self, token: dict[str, Any], app: Application, source: OAuthSource):
|
||||||
"""Create user from JWT"""
|
"""Create user from JWT"""
|
||||||
self.user, created = User.objects.update_or_create(
|
with audit_ignore():
|
||||||
username=f"{self.provider.name}-{token.get('sub')}",
|
self.user, created = User.objects.update_or_create(
|
||||||
defaults={
|
username=f"{self.provider.name}-{token.get('sub')}",
|
||||||
"attributes": {
|
defaults={
|
||||||
USER_ATTRIBUTE_GENERATED: True,
|
"attributes": {
|
||||||
|
USER_ATTRIBUTE_GENERATED: True,
|
||||||
|
},
|
||||||
|
"last_login": timezone.now(),
|
||||||
|
"name": (
|
||||||
|
f"Autogenerated user from application {app.name} (client credentials JWT)"
|
||||||
|
),
|
||||||
|
"path": source.get_user_path(),
|
||||||
|
"type": UserTypes.SERVICE_ACCOUNT,
|
||||||
},
|
},
|
||||||
"last_login": timezone.now(),
|
)
|
||||||
"name": f"Autogenerated user from application {app.name} (client credentials JWT)",
|
exp = token.get("exp")
|
||||||
"path": source.get_user_path(),
|
if created and exp:
|
||||||
"type": UserTypes.SERVICE_ACCOUNT,
|
self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp
|
||||||
},
|
self.user.save()
|
||||||
)
|
|
||||||
exp = token.get("exp")
|
|
||||||
if created and exp:
|
|
||||||
self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp
|
|
||||||
self.user.save()
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(csrf_exempt, name="dispatch")
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
|
|||||||
@ -21,6 +21,7 @@ from webauthn.helpers.structs import UserVerificationRequirement
|
|||||||
from authentik.core.api.utils import JSONDictField, PassiveSerializer
|
from authentik.core.api.utils import JSONDictField, PassiveSerializer
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application, User
|
||||||
from authentik.core.signals import login_failed
|
from authentik.core.signals import login_failed
|
||||||
|
from authentik.events.middleware import audit_ignore
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
|
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
|
||||||
@ -167,7 +168,8 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
|
|||||||
)
|
)
|
||||||
raise ValidationError("Assertion failed") from exc
|
raise ValidationError("Assertion failed") from exc
|
||||||
|
|
||||||
device.set_sign_count(authentication_verification.new_sign_count)
|
with audit_ignore():
|
||||||
|
device.set_sign_count(authentication_verification.new_sign_count)
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user