Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom data and actor email #603

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions auditlog/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,19 @@ class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin):
f"actor__{get_user_model().USERNAME_FIELD}",
]
list_filter = ["action", ResourceTypeFilter, CIDFilter]
readonly_fields = ["created", "resource_url", "action", "user_url", "msg"]
readonly_fields = [
"created",
"resource_url",
"action",
"user_url",
"msg",
"custom_data",
]
fieldsets = [
(None, {"fields": ["created", "user_url", "resource_url", "cid"]}),
(
None,
{"fields": ["created", "user_url", "resource_url", "custom_data", "cid"]},
),
(_("Changes"), {"fields": ["action", "msg"]}),
]

Expand Down
39 changes: 30 additions & 9 deletions auditlog/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from functools import partial

from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.db.models.signals import pre_save

from auditlog.models import LogEntry
Expand All @@ -14,23 +15,41 @@

@contextlib.contextmanager
def set_actor(actor, remote_addr=None):
yield from _set_logger_data(actor, {}, remote_addr)


@contextlib.contextmanager
def set_auditlog_custom_data(actor: User = None, remote_addr: str = None, **kwargs):
yield from _set_logger_data(actor, kwargs, remote_addr)


def _set_logger_data(actor, kwargs, remote_addr):
try:
context_data = auditlog_value.get()
except LookupError:
context_data = {}
actor = actor or context_data.get("actor")
custom_data = context_data.get("custom_data", {})
custom_data.update(kwargs)
"""Connect a signal receiver with current user attached."""
# Initialize thread local storage
context_data = {
"signal_duid": ("set_actor", time.time()),
"signal_duid": ("set_auditlog_custom_data", time.time()),
"remote_addr": remote_addr,
"custom_data": custom_data,
}
auditlog_value.set(context_data)

if actor:
context_data["actor"] = actor
token = auditlog_value.set(context_data)
# Connect signal for automatic logging
set_actor = partial(_set_actor, user=actor, signal_duid=context_data["signal_duid"])
set_auditlog_custom_data = partial(
_set_auditlog_custom_data, user=actor, signal_duid=context_data["signal_duid"]
)
pre_save.connect(
set_actor,
set_auditlog_custom_data,
sender=LogEntry,
dispatch_uid=context_data["signal_duid"],
weak=False,
)

try:
yield
finally:
Expand All @@ -40,9 +59,10 @@ def set_actor(actor, remote_addr=None):
pass
else:
pre_save.disconnect(sender=LogEntry, dispatch_uid=auditlog["signal_duid"])
auditlog_value.reset(token)


def _set_actor(user, sender, instance, signal_duid, **kwargs):
def _set_auditlog_custom_data(user: User, sender, instance, signal_duid, **kwargs):
"""Signal receiver with extra 'user' and 'signal_duid' kwargs.

This function becomes a valid signal receiver when it is curried with the actor and a dispatch id.
Expand All @@ -61,8 +81,9 @@ def _set_actor(user, sender, instance, signal_duid, **kwargs):
and instance.actor is None
):
instance.actor = user

instance.actor_email = user.email
instance.remote_addr = auditlog["remote_addr"]
instance.custom_data = auditlog["custom_data"]


@contextlib.contextmanager
Expand Down
20 changes: 20 additions & 0 deletions auditlog/migrations/0016_add_actor_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("auditlog", "0015_alter_logentry_changes"),
]

operations = [
migrations.AddField(
model_name="logentry",
name="actor_email",
field=models.CharField(
null=True,
verbose_name="actor email",
blank=True,
max_length=254,
),
),
]
19 changes: 19 additions & 0 deletions auditlog/migrations/0017_add_custom_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("auditlog", "0016_add_actor_email"),
]

operations = [
migrations.AddField(
model_name="logentry",
name="custom_data",
field=models.JSONField(
null=True,
verbose_name="custom data",
blank=True,
),
),
]
2 changes: 2 additions & 0 deletions auditlog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,8 @@ class Action:
additional_data = models.JSONField(
blank=True, null=True, verbose_name=_("additional data")
)
actor_email = models.CharField(blank=True, null=True, max_length=254)
custom_data = models.JSONField(blank=True, null=True)

objects = LogEntryManager()

Expand Down
15 changes: 14 additions & 1 deletion auditlog_tests/test_two_step_json_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,20 @@ def call_command(self, *args, **kwargs):
call_command(
"auditlogmigratejson", *args, stdout=outbuf, stderr=errbuf, **kwargs
)
return outbuf.getvalue().strip(), errbuf.getvalue().strip()
outbuf = self._remove_formatters(outbuf)
errbuf = self._remove_formatters(errbuf)
return outbuf, errbuf

@staticmethod
def _remove_formatters(outbuf):
return (
outbuf.getvalue()
.strip()
.replace("\x1b[0m", "")
.replace("\x1b[32;1m", "")
.replace("\x1b[33;1m", "")
.replace("\x1b[31;1m", "")
)

def test_nothing_to_migrate(self):
outbuf, errbuf = self.call_command()
Expand Down
27 changes: 26 additions & 1 deletion auditlog_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

from auditlog.admin import LogEntryAdmin
from auditlog.cid import get_cid
from auditlog.context import disable_auditlog, set_actor
from auditlog.context import disable_auditlog, set_actor, set_auditlog_custom_data
from auditlog.diff import model_instance_diff
from auditlog.middleware import AuditlogMiddleware
from auditlog.models import DEFAULT_OBJECT_REPR, LogEntry
Expand Down Expand Up @@ -581,6 +581,31 @@ def test_set_actor_anonymous_request(self):
)
self.assertIsNone(history.actor, msg="Actor is `None` for anonymous user")

def test_set_actor_get_email(self):
"""
The remote address will be set even when there is no actor
"""
actor = self.user

with set_actor(actor=actor):
obj = SimpleModel.objects.create(text="I am not difficult.")

history = obj.history.get()
self.assertEqual(history.actor_email, self.user.email)

def test_set_actor_set_custom_data(self):
"""
The remote address will be set even when there is no actor
"""
actor = self.user

with set_auditlog_custom_data(actor=actor, custom_data={"foo": "bar"}):
obj = SimpleModel.objects.create(text="I am not difficult.")

history = obj.history.get()
self.assertEqual(history.actor_email, self.user.email)
self.assertEqual(history.custom_data, {"custom_data": {"foo": "bar"}})

def test_get_actor(self):
params = [
(AnonymousUser(), None, "The user is anonymous so the actor is `None`"),
Expand Down
Loading