mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-05-08 15:44:44 +00:00
Merge d9343c098b into 7e33d8261e
This commit is contained in:
commit
288170c748
5 changed files with 145 additions and 11 deletions
|
|
@ -58,10 +58,7 @@ class LogEntryManager(models.Manager):
|
|||
"content_type", ContentType.objects.get_for_model(instance)
|
||||
)
|
||||
kwargs.setdefault("object_pk", pk)
|
||||
try:
|
||||
object_repr = smart_str(instance)
|
||||
except ObjectDoesNotExist:
|
||||
object_repr = DEFAULT_OBJECT_REPR
|
||||
object_repr = _get_object_repr_value(instance)
|
||||
kwargs.setdefault("object_repr", object_repr)
|
||||
kwargs.setdefault(
|
||||
"serialized_data", self._get_serialized_data_or_none(instance)
|
||||
|
|
@ -104,10 +101,7 @@ class LogEntryManager(models.Manager):
|
|||
"content_type", ContentType.objects.get_for_model(instance)
|
||||
)
|
||||
kwargs.setdefault("object_pk", pk)
|
||||
try:
|
||||
object_repr = smart_str(instance)
|
||||
except ObjectDoesNotExist:
|
||||
object_repr = DEFAULT_OBJECT_REPR
|
||||
object_repr = _get_object_repr_value(instance)
|
||||
kwargs.setdefault("object_repr", object_repr)
|
||||
kwargs.setdefault("action", LogEntry.Action.UPDATE)
|
||||
|
||||
|
|
@ -118,7 +112,9 @@ class LogEntryManager(models.Manager):
|
|||
if callable(get_additional_data):
|
||||
kwargs.setdefault("additional_data", get_additional_data())
|
||||
|
||||
objects = [smart_str(instance) for instance in changed_queryset]
|
||||
objects = [
|
||||
_get_object_repr_value(instance) for instance in changed_queryset
|
||||
]
|
||||
kwargs["changes"] = {
|
||||
field_name: {
|
||||
"type": "m2m",
|
||||
|
|
@ -566,7 +562,7 @@ class AbstractLogEntry(models.Model):
|
|||
try:
|
||||
related_model_manager = _get_manager_from_settings(field.related_model)
|
||||
|
||||
return smart_str(related_model_manager.get(pk=pk_value))
|
||||
return _get_object_repr_value(related_model_manager.get(pk=pk_value))
|
||||
# ObjectDoesNotExist will be raised if the object was deleted.
|
||||
except ObjectDoesNotExist:
|
||||
return f"Deleted '{field.related_model.__name__}' ({value})"
|
||||
|
|
@ -653,3 +649,28 @@ def _get_manager_from_settings(model: type[models.Model]) -> models.Manager:
|
|||
return model._meta.base_manager
|
||||
else:
|
||||
return model._meta.default_manager
|
||||
|
||||
|
||||
def _get_object_repr_value(instance):
|
||||
"""
|
||||
Get the object_repr value of the given model instance. The default behavior is to stringify the
|
||||
instance, unless an explicit object_repr_field is given when registering the model onto the audit log.
|
||||
|
||||
:param instance: The model instance to get the object_repr value for.
|
||||
:type instance: Model
|
||||
:return: The object_repr value of the given model instance.
|
||||
"""
|
||||
from auditlog.registry import auditlog
|
||||
|
||||
target = instance
|
||||
try:
|
||||
if auditlog.contains(instance.__class__):
|
||||
model_fields = auditlog.get_model_fields(instance.__class__)
|
||||
object_repr_field = model_fields["object_repr_field"]
|
||||
if object_repr_field:
|
||||
target = getattr(instance, object_repr_field, DEFAULT_OBJECT_REPR)
|
||||
if callable(target):
|
||||
target = target()
|
||||
return smart_str(target)
|
||||
except ObjectDoesNotExist:
|
||||
return DEFAULT_OBJECT_REPR
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ class AuditlogModelRegistry:
|
|||
mask_fields: list[str] | None = None,
|
||||
mask_callable: str | None = None,
|
||||
m2m_fields: Collection[str] | None = None,
|
||||
object_repr_field: str | None = None,
|
||||
serialize_data: bool = False,
|
||||
serialize_kwargs: dict[str, Any] | None = None,
|
||||
serialize_auditlog_fields_only: bool = False,
|
||||
|
|
@ -82,6 +83,8 @@ class AuditlogModelRegistry:
|
|||
:param mask_fields: The fields to mask for sensitive info.
|
||||
:param mask_callable: The dotted path to a callable that will be used for masking. If not provided,
|
||||
the default mask_callable will be used.
|
||||
:param object_repr_field: The field or method to represent the instance in the log entry's object_repr
|
||||
field, when absent the instance is stringified
|
||||
:param m2m_fields: The fields to handle as many to many.
|
||||
:param serialize_data: Option to include a dictionary of the objects state in the auditlog.
|
||||
:param serialize_kwargs: Optional kwargs to pass to Django serializer
|
||||
|
|
@ -125,6 +128,7 @@ class AuditlogModelRegistry:
|
|||
"mask_fields": mask_fields,
|
||||
"mask_callable": mask_callable,
|
||||
"m2m_fields": m2m_fields,
|
||||
"object_repr_field": object_repr_field,
|
||||
"serialize_data": serialize_data,
|
||||
"serialize_kwargs": serialize_kwargs,
|
||||
"serialize_auditlog_fields_only": serialize_auditlog_fields_only,
|
||||
|
|
@ -177,6 +181,7 @@ class AuditlogModelRegistry:
|
|||
"mapping_fields": dict(self._registry[model]["mapping_fields"]),
|
||||
"mask_fields": list(self._registry[model]["mask_fields"]),
|
||||
"mask_callable": self._registry[model]["mask_callable"],
|
||||
"object_repr_field": self._registry[model]["object_repr_field"],
|
||||
}
|
||||
|
||||
def get_serialize_options(self, model: ModelBase):
|
||||
|
|
|
|||
|
|
@ -483,6 +483,20 @@ class NullableFieldModel(models.Model):
|
|||
history = AuditlogHistoryField(delete_related=True)
|
||||
|
||||
|
||||
class ObjectReprOverrideModel(models.Model):
|
||||
public_field = models.CharField(max_length=255)
|
||||
sensitive_field = models.CharField(max_length=255)
|
||||
related = models.ManyToManyField("self", blank=True)
|
||||
parent = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True)
|
||||
history = AuditlogHistoryField()
|
||||
|
||||
def some_callable(self):
|
||||
return f"{self.sensitive_field[0:3]}..."
|
||||
|
||||
def __str__(self):
|
||||
return self.sensitive_field
|
||||
|
||||
|
||||
auditlog.register(AltPrimaryKeyModel)
|
||||
auditlog.register(UUIDPrimaryKeyModel)
|
||||
auditlog.register(ModelPrimaryKeyModel)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from test_app.models import (
|
|||
ModelPrimaryKeyModel,
|
||||
NoDeleteHistoryModel,
|
||||
NullableJSONModel,
|
||||
ObjectReprOverrideModel,
|
||||
ProxyModel,
|
||||
RelatedModel,
|
||||
RelatedModelParent,
|
||||
|
|
@ -417,6 +418,87 @@ class ModelPrimaryKeyTest(TransactionTestCase):
|
|||
self.assertEqual(pk, key.pk)
|
||||
|
||||
|
||||
class BaseObjectReprOverrideTests:
|
||||
object_repr_field = None
|
||||
|
||||
def get_expected_repr(self, instance):
|
||||
raise NotImplementedError
|
||||
|
||||
def setUp(self):
|
||||
auditlog.register(
|
||||
ObjectReprOverrideModel,
|
||||
object_repr_field=self.object_repr_field,
|
||||
m2m_fields=["related"],
|
||||
)
|
||||
self.obj = ObjectReprOverrideModel.objects.create(
|
||||
public_field="public information",
|
||||
sensitive_field="secret information",
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
auditlog.unregister(ObjectReprOverrideModel)
|
||||
|
||||
def test_object_repr_uses_override(self):
|
||||
self.assertEqual(
|
||||
self.obj.history.latest().object_repr,
|
||||
self.get_expected_repr(self.obj),
|
||||
msg=f"Failed overriding object_repr with config: {self.object_repr_field}",
|
||||
)
|
||||
|
||||
def test_m2m_changes_dict_uses_object_repr_field(self):
|
||||
second = ObjectReprOverrideModel.objects.create(
|
||||
public_field="second node", sensitive_field="second secret"
|
||||
)
|
||||
self.obj.related.add(second)
|
||||
log_entry = self.obj.history.latest()
|
||||
self.assertEqual(
|
||||
log_entry.changes["related"]["objects"],
|
||||
[self.get_expected_repr(second)],
|
||||
msg=f"Failed M2M objects array with config: {self.object_repr_field}",
|
||||
)
|
||||
|
||||
def test_fk_changes_display_dict_uses_object_repr_field(self):
|
||||
second = ObjectReprOverrideModel.objects.create(
|
||||
public_field="second node", sensitive_field="second secret"
|
||||
)
|
||||
second.parent = self.obj
|
||||
second.save()
|
||||
log_entry = second.history.latest()
|
||||
self.assertEqual(
|
||||
log_entry.changes_display_dict["parent"][1],
|
||||
self.get_expected_repr(self.obj),
|
||||
msg=f"Failed FK changes display dict with config: {self.object_repr_field}",
|
||||
)
|
||||
|
||||
|
||||
class NoObjectReprOverrideTest(BaseObjectReprOverrideTests, TestCase):
|
||||
object_repr_field = None
|
||||
|
||||
def get_expected_repr(self, instance):
|
||||
return smart_str(instance)
|
||||
|
||||
|
||||
class ObjectReprFromPublicFieldTest(BaseObjectReprOverrideTests, TestCase):
|
||||
object_repr_field = "public_field"
|
||||
|
||||
def get_expected_repr(self, instance):
|
||||
return instance.public_field
|
||||
|
||||
|
||||
class ObjectReprFromCallableTest(BaseObjectReprOverrideTests, TestCase):
|
||||
object_repr_field = "some_callable"
|
||||
|
||||
def get_expected_repr(self, instance):
|
||||
return instance.some_callable()
|
||||
|
||||
|
||||
class ObjectReprFromMissingFieldTest(BaseObjectReprOverrideTests, TestCase):
|
||||
object_repr_field = "bogus_missing_field"
|
||||
|
||||
def get_expected_repr(self, instance):
|
||||
return DEFAULT_OBJECT_REPR
|
||||
|
||||
|
||||
class ProxyModelBase(SimpleModelTest):
|
||||
def make_object(self):
|
||||
return ProxyModel.objects.create(text="I am not what you think.")
|
||||
|
|
@ -1399,7 +1481,7 @@ class RegisterModelSettingsTest(TestCase):
|
|||
|
||||
self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel))
|
||||
self.assertTrue(self.test_auditlog.contains(ChoicesFieldModel))
|
||||
self.assertEqual(len(self.test_auditlog.get_models()), 36)
|
||||
self.assertEqual(len(self.test_auditlog.get_models()), 37)
|
||||
|
||||
def test_register_models_register_model_with_attrs(self):
|
||||
self.test_auditlog._register_models(
|
||||
|
|
|
|||
|
|
@ -167,6 +167,18 @@ the first half of the string with asterisks.
|
|||
|
||||
Masking fields
|
||||
|
||||
**Overriding object_repr**
|
||||
|
||||
Some models may leak sensitive information to the audit logs through their builtin ``__str__`` method.
|
||||
|
||||
You can customize how the entries in the audit log summarize the objects by setting the ``object_repr_field`` when you register the model::
|
||||
|
||||
auditlog.register(MyModel, object_repr_field="safe_summary")
|
||||
|
||||
Alternatively you can set the field to a method::
|
||||
|
||||
auditlog.register(MyModel, object_repr_field="get_safe_summary")
|
||||
|
||||
**Many-to-many fields**
|
||||
|
||||
Changes to many-to-many fields are not tracked by default. If you want to enable tracking of a many-to-many field on a model, pass ``m2m_fields`` to the ``register`` method:
|
||||
|
|
|
|||
Loading…
Reference in a new issue