diff --git a/passbook/audit/migrations/0003_auto_20200917_1155.py b/passbook/audit/migrations/0003_auto_20200917_1155.py new file mode 100644 index 000000000..6fa60f1da --- /dev/null +++ b/passbook/audit/migrations/0003_auto_20200917_1155.py @@ -0,0 +1,59 @@ +# Generated by Django 3.1.1 on 2020-09-17 11:55 +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + +import passbook.audit.models + + +def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): + Event = apps.get_model("passbook_audit", "Event") + + db_alias = schema_editor.connection.alias + for event in Event.objects.all(): + event.delete() + # Because event objects cannot be updated, we have to re-create them + event.pk = None + event.user_json = ( + passbook.audit.models.get_user(event.user) if event.user else {} + ) + event._state.adding = True + event.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_audit", "0002_auto_20200918_2116"), + ] + + 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"), + ] + ), + ), + migrations.AddField( + model_name="event", name="user_json", field=models.JSONField(default=dict), + ), + migrations.RunPython(convert_user_to_json), + migrations.RemoveField(model_name="event", name="user",), + migrations.RenameField( + model_name="event", old_name="user_json", new_name="user" + ), + ] diff --git a/passbook/audit/models.py b/passbook/audit/models.py index 125bf9162..90f363ce7 100644 --- a/passbook/audit/models.py +++ b/passbook/audit/models.py @@ -12,13 +12,14 @@ 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 guardian.utils import get_anonymous_user from structlog import get_logger from passbook.core.middleware import ( SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER, ) +from passbook.core.models import User from passbook.lib.utils.http import get_client_ip LOGGER = get_logger() @@ -53,6 +54,22 @@ def model_to_dict(model: Model) -> Dict[str, Any]: } +def get_user(user: User, original_user: Optional[User] = None) -> Dict[str, Any]: + """Convert user object to dictionary, optionally including the original user""" + if isinstance(user, AnonymousUser): + user = get_anonymous_user() + user_data = { + "username": user.username, + "pk": user.pk, + "email": user.email, + } + if original_user: + original_data = get_user(original_user) + original_data["on_behalf_of"] = user_data + return original_data + return user_data + + 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 { @@ -101,9 +118,7 @@ class Event(models.Model): """An individual audit log event""" event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL - ) + user = models.JSONField(default=dict) action = models.TextField(choices=EventAction.as_choices()) date = models.DateTimeField(auto_now_add=True) app = models.TextField() @@ -142,17 +157,17 @@ class Event(models.Model): Events independently from requests. `user` arguments optionally overrides user from requests.""" if hasattr(request, "user"): - if isinstance(request.user, AnonymousUser): - self.user = get_anonymous_user() - else: - self.user = request.user + self.user = get_user( + request.user, + request.session.get(SESSION_IMPERSONATE_ORIGINAL_USER, None), + ) if user: - self.user = user + self.user = get_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.user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER] - self.context["on_behalf_of"] = model_to_dict( + self.user = get_user(request.session[SESSION_IMPERSONATE_ORIGINAL_USER]) + self.user["on_behalf_of"] = get_user( request.session[SESSION_IMPERSONATE_USER] ) # User 255.255.255.255 as fallback if IP cannot be determined diff --git a/passbook/audit/signals.py b/passbook/audit/signals.py index d0687739a..d864e8f21 100644 --- a/passbook/audit/signals.py +++ b/passbook/audit/signals.py @@ -57,7 +57,9 @@ def on_user_logged_out(sender, request: HttpRequest, user: User, **_): # pylint: disable=unused-argument def on_user_write(sender, request: HttpRequest, user: User, data: Dict[str, Any], **_): """Log User write""" - thread = EventNewThread(EventAction.CUSTOM, request, **data) + thread = EventNewThread( + EventAction.CUSTOM, request, caller="stages/user_write", **data + ) thread.user = user thread.run() diff --git a/passbook/audit/templates/audit/list.html b/passbook/audit/templates/audit/list.html index 408195272..b42c93cd3 100644 --- a/passbook/audit/templates/audit/list.html +++ b/passbook/audit/templates/audit/list.html @@ -40,12 +40,28 @@ - {{ entry.context }} +
+
+ {{ entry.context }} +
+ {% if entry.user.on_behalf_of %} + + {% blocktrans with username=entry.user.on_behalf_of.username %} + On behalf of {{ username }} + {% endblocktrans %} + + {% endif %} +
- - {{ entry.user }} - +
+
{{ entry.user.username }}
+ + {% blocktrans with pk=entry.user.pk %} + ID: {{ pk }} + {% endblocktrans %} + +