From fe4a0c3b44bbc4bb6d08dbaf00ef272a8bbbfc52 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 18 Sep 2020 23:39:37 +0200 Subject: [PATCH] core: add impersonation start/end to audit log also add impersonated user as context to other logs --- .../migrations/0002_auto_20200918_2116.py | 33 +++++++++++++++++ passbook/audit/models.py | 37 ++++++++++++------- passbook/core/views/impersonate.py | 5 ++- swagger.yaml | 2 + 4 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 passbook/audit/migrations/0002_auto_20200918_2116.py diff --git a/passbook/audit/migrations/0002_auto_20200918_2116.py b/passbook/audit/migrations/0002_auto_20200918_2116.py new file mode 100644 index 000000000..d645c11c0 --- /dev/null +++ b/passbook/audit/migrations/0002_auto_20200918_2116.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1.1 on 2020-09-18 21:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_audit", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="event", + name="action", + field=models.TextField( + choices=[ + ("LOGIN", "login"), + ("LOGIN_FAILED", "login_failed"), + ("LOGOUT", "logout"), + ("AUTHORIZE_APPLICATION", "authorize_application"), + ("SUSPICIOUS_REQUEST", "suspicious_request"), + ("SIGN_UP", "sign_up"), + ("PASSWORD_RESET", "password_reset"), + ("INVITE_CREATED", "invitation_created"), + ("INVITE_USED", "invitation_used"), + ("IMPERSONATION_STARTED", "impersonation_started"), + ("IMPERSONATION_ENDED", "impersonation_ended"), + ("CUSTOM", "custom"), + ] + ), + ), + ] diff --git a/passbook/audit/models.py b/passbook/audit/models.py index d1b78cd8c..f8e5d0870 100644 --- a/passbook/audit/models.py +++ b/passbook/audit/models.py @@ -6,15 +6,16 @@ from uuid import UUID, uuid4 from django.conf import settings from django.contrib.auth.models import AnonymousUser -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import models +from django.db.models.base import Model from django.http import HttpRequest from django.utils.translation import gettext as _ from django.views.debug import SafeExceptionReporterFilter from guardian.shortcuts import get_anonymous_user from structlog import get_logger +from passbook.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER from passbook.lib.utils.http import get_client_ip LOGGER = get_logger() @@ -36,6 +37,19 @@ def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: return final_dict +def model_to_dict(model: Model) -> Dict[str, Any]: + """Convert model to dict""" + name = str(model) + if hasattr(model, "name"): + name = model.name + return { + "app": model._meta.app_label, + "model_name": model._meta.model_name, + "pk": model.pk, + "name": name, + } + + def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: """clean source of all Models that would interfere with the JSONField. Models are replaced with a dictionary of { @@ -48,18 +62,7 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: if isinstance(value, dict): final_dict[key] = sanitize_dict(value) elif isinstance(value, models.Model): - model_content_type = ContentType.objects.get_for_model(value) - name = str(value) - if hasattr(value, "name"): - name = value.name - final_dict[key] = sanitize_dict( - { - "app": model_content_type.app_label, - "model_name": model_content_type.model, - "pk": value.pk, - "name": name, - } - ) + final_dict[key] = sanitize_dict(model_to_dict(value)) elif isinstance(value, UUID): final_dict[key] = value.hex else: @@ -79,6 +82,8 @@ class EventAction(Enum): PASSWORD_RESET = "password_reset" # noqa # nosec INVITE_CREATED = "invitation_created" INVITE_USED = "invitation_used" + IMPERSONATION_STARTED = "impersonation_started" + IMPERSONATION_ENDED = "impersonation_ended" CUSTOM = "custom" @staticmethod @@ -140,6 +145,12 @@ class Event(models.Model): self.user = request.user if user: self.user = user + # Check if we're currently impersonating, and add that user + if hasattr(request, "session"): + if SESSION_IMPERSONATE_ORIGINAL_USER in request.session: + self.context["on_behalf_of"] = model_to_dict( + request.session[SESSION_IMPERSONATE_ORIGINAL_USER] + ) # User 255.255.255.255 as fallback if IP cannot be determined self.client_ip = get_client_ip(request) or "255.255.255.255" # If there's no app set, we get it from the requests too diff --git a/passbook/core/views/impersonate.py b/passbook/core/views/impersonate.py index 009c74d2a..49fcc2ab9 100644 --- a/passbook/core/views/impersonate.py +++ b/passbook/core/views/impersonate.py @@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404, redirect from django.views import View from structlog import get_logger +from passbook.audit.models import Event, EventAction from passbook.core.middleware import ( SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER, @@ -30,7 +31,7 @@ class ImpersonateInitView(View): request.session[SESSION_IMPERSONATE_ORIGINAL_USER] = request.user request.session[SESSION_IMPERSONATE_USER] = user_to_be - # TODO Audit log entry + Event.new(EventAction.IMPERSONATION_STARTED).from_http(request) return redirect("passbook_core:overview") @@ -50,6 +51,6 @@ class ImpersonateEndView(View): del request.session[SESSION_IMPERSONATE_USER] del request.session[SESSION_IMPERSONATE_ORIGINAL_USER] - # TODO: Audit log entry + Event.new(EventAction.IMPERSONATION_ENDED).from_http(request) return redirect("passbook_core:overview") diff --git a/swagger.yaml b/swagger.yaml index d5ad29565..7479de03b 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -5846,6 +5846,8 @@ definitions: - PASSWORD_RESET - INVITE_CREATED - INVITE_USED + - IMPERSONATION_STARTED + - IMPERSONATION_ENDED - CUSTOM date: title: Date