Skip to content

Commit

Permalink
events: add context manager to ignore/modify audit events being writt…
Browse files Browse the repository at this point in the history
…en (#9181)
  • Loading branch information
BeryJu authored Apr 8, 2024
1 parent 16b8edd commit 2ec8a44
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 24 deletions.
39 changes: 39 additions & 0 deletions authentik/events/middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Events middleware"""

from collections.abc import Callable
from contextlib import contextmanager
from contextvars import ContextVar
from functools import partial
from threading import Thread
from typing import Any
Expand Down Expand Up @@ -31,6 +33,9 @@
)
)

_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:
"""Return true if operation on `model` should be logged"""
Expand All @@ -44,6 +49,28 @@ def should_log_m2m(model: Model) -> bool:
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):
"""Create Event in background thread"""

Expand Down Expand Up @@ -158,6 +185,10 @@ def post_save_handler(
"""Signal handler for all object's post_save"""
if not should_log_model(instance):
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
thread = EventNewThread(action, request, user=user, model=model_to_dict(instance))
Expand All @@ -168,6 +199,10 @@ def pre_delete_handler(self, user: User, request: HttpRequest, sender, instance:
"""Signal handler for all object's pre_delete"""
if not should_log_model(instance): # pragma: no cover
return
if _CTX_IGNORE.get():
return
if _new_user := _CTX_OVERWRITE_USER.get():
user = _new_user

EventNewThread(
EventAction.MODEL_DELETED,
Expand All @@ -184,6 +219,10 @@ def m2m_changed_handler(
return
if not should_log_m2m(instance):
return
if _CTX_IGNORE.get():
return
if _new_user := _CTX_OVERWRITE_USER.get():
user = _new_user

EventNewThread(
EventAction.MODEL_UPDATED,
Expand Down
57 changes: 49 additions & 8 deletions authentik/events/tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

from authentik.core.models import Application
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.lib.generators import generate_id


class TestEventsMiddleware(APITestCase):
Expand All @@ -15,35 +17,74 @@ def setUp(self) -> None:
super().setUp()
self.user = create_test_admin_user()
self.client.force_login(self.user)
Event.objects.all().delete()

def test_create(self):
"""Test model creation event"""
uid = generate_id()
self.client.post(
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(
Event.objects.filter(
action=EventAction.MODEL_CREATED,
context__model__model_name="application",
context__model__app="authentik_core",
context__model__name="test-create",
context__model__name=uid,
).exists()
)

def test_delete(self):
"""Test model creation event"""
Application.objects.create(name="test-delete", slug="test-delete")
self.client.delete(
reverse("authentik_api:application-detail", kwargs={"slug": "test-delete"})
)
uid = generate_id()
Application.objects.create(name=uid, slug=uid)
self.client.delete(reverse("authentik_api:application-detail", kwargs={"slug": uid}))
self.assertFalse(Application.objects.filter(name="test").exists())
self.assertTrue(
Event.objects.filter(
action=EventAction.MODEL_DELETED,
context__model__model_name="application",
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()
)
34 changes: 19 additions & 15 deletions authentik/providers/oauth2/views/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
User,
UserTypes,
)
from authentik.events.middleware import audit_ignore
from authentik.events.models import Event, EventAction
from authentik.events.signals import get_login_event
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION
Expand Down Expand Up @@ -465,22 +466,25 @@ def __post_init_device_code(self, request: HttpRequest):

def __create_user_from_jwt(self, token: dict[str, Any], app: Application, source: OAuthSource):
"""Create user from JWT"""
self.user, created = User.objects.update_or_create(
username=f"{self.provider.name}-{token.get('sub')}",
defaults={
"attributes": {
USER_ATTRIBUTE_GENERATED: True,
with audit_ignore():
self.user, created = User.objects.update_or_create(
username=f"{self.provider.name}-{token.get('sub')}",
defaults={
"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)",
"path": source.get_user_path(),
"type": UserTypes.SERVICE_ACCOUNT,
},
)
exp = token.get("exp")
if created and exp:
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")
Expand Down
4 changes: 3 additions & 1 deletion authentik/stages/authenticator_validate/challenge.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.core.models import Application, User
from authentik.core.signals import login_failed
from authentik.events.middleware import audit_ignore
from authentik.events.models import Event, EventAction
from authentik.flows.stage import StageView
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
Expand Down Expand Up @@ -167,7 +168,8 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
)
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


Expand Down

0 comments on commit 2ec8a44

Please sign in to comment.