Add mask_fields argument in register (#310)

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
This commit is contained in:
Amin Alaee 2022-03-10 14:17:50 +01:00 committed by GitHub
parent dad4fb893b
commit bb5f99533e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 68 additions and 1 deletions

View file

@ -2,6 +2,7 @@
#### Improvements
- feat: enable use of replica database (delegating the choice to `DATABASES_ROUTER`) ([#359](https://github.com/jazzband/django-auditlog/pull/359))
- Add `mask_fields` argument in `register` to mask sensitive information when logging ([#3710](https://github.com/jazzband/django-auditlog/pull/310))
#### Important notes
- LogEntry no longer save to same database instance is using

View file

@ -76,6 +76,19 @@ def get_field_value(obj, field):
return value
def mask_str(value: str) -> str:
"""
Masks the first half of the input string to remove sensitive data.
:param value: The value to mask.
:type value: str
:return: The masked version of the string.
:rtype: str
"""
mask_limit = int(len(value) / 2)
return "*" * mask_limit + value[mask_limit:]
def model_instance_diff(old, new, fields_to_check=None):
"""
Calculates the differences between two model instances. One of the instances may be ``None`` (i.e., a newly
@ -145,7 +158,13 @@ def model_instance_diff(old, new, fields_to_check=None):
new_value = get_field_value(new, field)
if old_value != new_value:
diff[field.name] = (smart_str(old_value), smart_str(new_value))
if model_fields and field.name in model_fields["mask_fields"]:
diff[field.name] = (
mask_str(smart_str(old_value)),
mask_str(smart_str(new_value)),
)
else:
diff[field.name] = (smart_str(old_value), smart_str(new_value))
if len(diff) == 0:
diff = None

View file

@ -40,6 +40,7 @@ class AuditlogModelRegistry:
include_fields: Optional[List[str]] = None,
exclude_fields: Optional[List[str]] = None,
mapping_fields: Optional[Dict[str, str]] = None,
mask_fields: Optional[List[str]] = None,
):
"""
Register a model with auditlog. Auditlog will then track mutations on this model's instances.
@ -48,6 +49,7 @@ class AuditlogModelRegistry:
:param include_fields: The fields to include. Implicitly excludes all other fields.
:param exclude_fields: The fields to exclude. Overrides the fields to include.
:param mapping_fields: Mapping from field names to strings in diff.
:param mask_fields: The fields to mask for sensitive info.
"""
@ -57,6 +59,8 @@ class AuditlogModelRegistry:
exclude_fields = []
if mapping_fields is None:
mapping_fields = {}
if mask_fields is None:
mask_fields = []
def registrar(cls):
"""Register models for a given class."""
@ -67,6 +71,7 @@ class AuditlogModelRegistry:
"include_fields": include_fields,
"exclude_fields": exclude_fields,
"mapping_fields": mapping_fields,
"mask_fields": mask_fields,
}
self._connect_signals(cls)
@ -114,6 +119,7 @@ class AuditlogModelRegistry:
"include_fields": list(self._registry[model]["include_fields"]),
"exclude_fields": list(self._registry[model]["exclude_fields"]),
"mapping_fields": dict(self._registry[model]["mapping_fields"]),
"mask_fields": list(self._registry[model]["mask_fields"]),
}
def _connect_signals(self, model):

View file

@ -115,6 +115,18 @@ class SimpleMappingModel(models.Model):
history = AuditlogHistoryField()
@auditlog.register(mask_fields=["address"])
class SimpleMaskedModel(models.Model):
"""
A simple model used for register's mask_fields kwarg
"""
address = models.CharField(max_length=100)
text = models.TextField()
history = AuditlogHistoryField()
class AdditionalDataIncludedModel(models.Model):
"""
A model where get_additional_data is defined which allows for logging extra

View file

@ -30,6 +30,7 @@ from auditlog_tests.models import (
SimpleExcludeModel,
SimpleIncludeModel,
SimpleMappingModel,
SimpleMaskedModel,
SimpleModel,
UUIDPrimaryKeyModel,
)
@ -413,6 +414,19 @@ class SimpleMappingModelTest(TestCase):
)
class SimpeMaskedFieldsModelTest(TestCase):
"""Log masked changes for fields in mask_fields"""
def test_register_mask_fields(self):
smm = SimpleMaskedModel(address="Sensitive data", text="Looong text")
smm.save()
self.assertEqual(
smm.history.latest().changes_dict["address"][1],
"*******ve data",
msg="The diff function masks 'address' field.",
)
class AdditionalDataModelTest(TestCase):
"""Log additional data if get_additional_data is defined in the model"""

View file

@ -76,6 +76,21 @@ during the `register()` call.
You do not need to map all the fields of the model, any fields not mapped will fall back on their ``verbose_name``. Django provides a default ``verbose_name`` which is a "munged camel case version" so ``product_name`` would become ``Product Name`` by default.
**Masking fields**
Fields that contain sensitive info and we want keep track of field change but not to contain the exact change.
To mask specific fields from the log you can pass ``mask_fields`` to the ``register``
method. If ``mask_fields`` is specified, the first half value of the fields is masked using ``*``.
For example, to mask the field ``address``, use::
auditlog.register(MyModel, mask_fields=['address'])
.. versionadded:: 1.1.0
Masking fields
Actors
------