From c4907bcd5222113ca977d941376bac9a6fae76a7 Mon Sep 17 00:00:00 2001 From: Amirreza Ashouri Date: Mon, 24 Feb 2025 12:05:04 +0330 Subject: [PATCH] Add ability to globally mask fields by name on all models. (#702) * Add ability to globally mask fields by name on all models. Fixes https://github.com/jazzband/django-auditlog/issues/701 * Add feature explanation in `usage.rst` file. * Add a record to CHANGELOG.md file. * Add test coverage. --- CHANGELOG.md | 2 ++ auditlog/conf.py | 5 +++++ auditlog/registry.py | 17 +++++++++++++++++ auditlog_tests/tests.py | 36 ++++++++++++++++++++++++++++++++++++ docs/source/usage.rst | 22 ++++++++++++++++++++++ 5 files changed, 82 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ed908..fd704c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ #### Improvements +- feat: Support masking field names globally when ```AUDITLOG_INCLUDE_ALL_MODELS``` is enabled +via `AUDITLOG_MASK_TRACKING_FIELDS` setting. ([#702](https://github.com/jazzband/django-auditlog/pull/702)) - feat: Added 'LogEntry.actor_email` field. ([#641](https://github.com/jazzband/django-auditlog/pull/641)) - Add Python 3.13 support. ([#671](https://github.com/jazzband/django-auditlog/pull/671)) - feat: Added `LogEntry.remote_port` field. ([#671](https://github.com/jazzband/django-auditlog/pull/671)) diff --git a/auditlog/conf.py b/auditlog/conf.py index fe347cc..dceedd1 100644 --- a/auditlog/conf.py +++ b/auditlog/conf.py @@ -21,6 +21,11 @@ settings.AUDITLOG_EXCLUDE_TRACKING_FIELDS = getattr( settings, "AUDITLOG_EXCLUDE_TRACKING_FIELDS", () ) +# Mask named fields across all models +settings.AUDITLOG_MASK_TRACKING_FIELDS = getattr( + settings, "AUDITLOG_MASK_TRACKING_FIELDS", () +) + # Disable on raw save to avoid logging imports and similar settings.AUDITLOG_DISABLE_ON_RAW_SAVE = getattr( settings, "AUDITLOG_DISABLE_ON_RAW_SAVE", False diff --git a/auditlog/registry.py b/auditlog/registry.py index aa8b689..b472f72 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -107,6 +107,9 @@ class AuditlogModelRegistry: for fld in settings.AUDITLOG_EXCLUDE_TRACKING_FIELDS: exclude_fields.append(fld) + for fld in settings.AUDITLOG_MASK_TRACKING_FIELDS: + mask_fields.append(fld) + def registrar(cls): """Register models for a given class.""" if not issubclass(cls, Model): @@ -300,6 +303,15 @@ class AuditlogModelRegistry: "setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be set to 'True'" ) + if ( + settings.AUDITLOG_MASK_TRACKING_FIELDS + and not settings.AUDITLOG_INCLUDE_ALL_MODELS + ): + raise ValueError( + "In order to use 'AUDITLOG_MASK_TRACKING_FIELDS', " + "setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be set to 'True'" + ) + if not isinstance(settings.AUDITLOG_INCLUDE_TRACKING_MODELS, (list, tuple)): raise TypeError( "Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' must be a list or tuple" @@ -310,6 +322,11 @@ class AuditlogModelRegistry: "Setting 'AUDITLOG_EXCLUDE_TRACKING_FIELDS' must be a list or tuple" ) + if not isinstance(settings.AUDITLOG_MASK_TRACKING_FIELDS, (list, tuple)): + raise TypeError( + "Setting 'AUDITLOG_MASK_TRACKING_FIELDS' must be a list or tuple" + ) + for item in settings.AUDITLOG_INCLUDE_TRACKING_MODELS: if not isinstance(item, (str, dict)): raise TypeError( diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 53e9f36..1904059 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1347,6 +1347,24 @@ class RegisterModelSettingsTest(TestCase): ): self.test_auditlog.register_from_settings() + with override_settings( + AUDITLOG_INCLUDE_ALL_MODELS=True, + AUDITLOG_MASK_TRACKING_FIELDS="badvalue", + ): + with self.assertRaisesMessage( + TypeError, + "Setting 'AUDITLOG_MASK_TRACKING_FIELDS' must be a list or tuple", + ): + self.test_auditlog.register_from_settings() + + with override_settings(AUDITLOG_MASK_TRACKING_FIELDS=("token", "otp_secret")): + with self.assertRaisesMessage( + ValueError, + "In order to use 'AUDITLOG_MASK_TRACKING_FIELDS', " + "setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be set to 'True'", + ): + self.test_auditlog.register_from_settings() + with override_settings(AUDITLOG_INCLUDE_TRACKING_MODELS="str"): with self.assertRaisesMessage( TypeError, @@ -1424,6 +1442,24 @@ class RegisterModelSettingsTest(TestCase): ["datetime"], ) + @override_settings( + AUDITLOG_INCLUDE_ALL_MODELS=True, + AUDITLOG_MASK_TRACKING_FIELDS=("secret",), + ) + def test_register_from_settings_register_all_models_with_mask_tracking_fields( + self, + ): + self.test_auditlog.register_from_settings() + + self.assertEqual( + self.test_auditlog.get_model_fields(SimpleModel)["mask_fields"], + ["secret"], + ) + self.assertEqual( + self.test_auditlog.get_model_fields(AltPrimaryKeyModel)["mask_fields"], + ["secret"], + ) + @override_settings( AUDITLOG_INCLUDE_ALL_MODELS=True, AUDITLOG_EXCLUDE_TRACKING_MODELS=["auditlog_tests.SimpleExcludeModel"], diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 0c79a56..46bf038 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -235,6 +235,28 @@ It will be considered when ``AUDITLOG_DISABLE_REMOTE_ADDR`` is `True`. .. versionadded:: 3.0.0 +**AUDITLOG_MASK_TRACKING_FIELDS** + +You can use this setting to mask specific field values in all tracked models +while still logging changes. This is useful when models contain sensitive fields +like `password`, `api_key`, or `secret_token`` that should not be logged +in plain text but need to be auditable. + +When a masked field changes, its value will be replaced with a masked +representation (e.g., `****`) in the audit log instead of storing the actual value. + +This setting will be applied only when `AUDITLOG_INCLUDE_ALL_MODELS`` is `True`. + +.. code-block:: python + + AUDITLOG_MASK_TRACKING_FIELDS = ( + "password", + "api_key", + "secret_token" + ) + +.. versionadded:: 3.1.0 + **AUDITLOG_EXCLUDE_TRACKING_MODELS** You can use this setting to exclude models in registration process.