django-field-audit 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-field-audit might be problematic. Click here for more details.

@@ -0,0 +1,362 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-field-audit
3
+ Version: 1.4.0
4
+ Summary: Audit Field Changes on Django Models
5
+ Author-email: Joel Miller <jmiller@dimagi.com>, Simon Kelly <simongdkelly@gmail.com>, Graham Herceg <gherceg@dimagi.com>, Chris Smit <chris.smit@dimagi.com>, Daniel Miller <millerdev@gmail.com>
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Framework :: Django
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ License-File: LICENSE
22
+ Requires-Dist: django>=3.2
23
+ Project-URL: Homepage, https://github.com/dimagi/django-field-audit
24
+ Project-URL: Repository, https://github.com/dimagi/django-field-audit
25
+
26
+ # Audit Field Changes on Django Models
27
+
28
+ [![tests][tests_badge]][tests_link]
29
+ [![coverage][coverage_badge]][coverage_link]
30
+ [![pypi package][pypi_badge]][pypi_link]
31
+
32
+ [tests_badge]: https://github.com/dimagi/django-field-audit/actions/workflows/tests.yml/badge.svg
33
+ [tests_link]: https://github.com/dimagi/django-field-audit/actions/workflows/tests.yml
34
+ [coverage_badge]: https://github.com/dimagi/django-field-audit/raw/coverage-badge/coverage.svg
35
+ [coverage_link]: https://github.com/dimagi/django-field-audit/actions/workflows/coverage.yml
36
+ [pypi_badge]: https://badge.fury.io/py/django-field-audit.svg
37
+ [pypi_link]: https://pypi.org/project/django-field-audit/
38
+
39
+ A Django app for auditing field changes on database models.
40
+
41
+ ## Installation
42
+ ```
43
+ pip install django-field-audit
44
+ ```
45
+
46
+ ## Documentation
47
+
48
+ <!--
49
+ The [django-field-audit documentation][docs] shows how to use this library to
50
+ audit field changes on Django Models.
51
+
52
+ [docs]: https://dimagi.github.io/django-field-audit/
53
+ -->
54
+
55
+ ### Django Settings
56
+
57
+ To enable the app, add it to your Django `INSTALLED_APPS` configuration and run
58
+ migrations. Settings example:
59
+
60
+ ```python
61
+ INSTALLED_APPS = [
62
+ # ...
63
+ "field_audit",
64
+ ]
65
+ ```
66
+
67
+ The "auditor chain" (see `FIELD_AUDIT_AUDITORS` in the **Custom settings** table
68
+ below) is configured out of the box with the default auditors. If
69
+ `change_context` auditing is desired for authenticated Django requests, add the
70
+ app middleware to your Django `MIDDLEWARE` configuration. For example:
71
+
72
+ ```python
73
+ MIDDLEWARE = [
74
+ # ...
75
+ "field_audit.middleware.FieldAuditMiddleware",
76
+ ]
77
+ ```
78
+
79
+ The audit chain can be updated to use custom auditors (subclasses of
80
+ `field_audit.auditors.BaseAuditor`). If `change_context` auditing is not
81
+ desired, the audit chain can be cleared to avoid extra processing:
82
+
83
+ ```python
84
+ FIELD_AUDIT_AUDITORS = []
85
+ ```
86
+
87
+ #### Custom settings details
88
+
89
+ | Name | Description | Default value when unset
90
+ |:----------------------------------|:---------------------------------------------------------------|:------------------------
91
+ | `FIELD_AUDIT_AUDITEVENT_MANAGER` | A custom manager to use for the `AuditEvent` Model. | `field_audit.models.DefaultAuditEventManager`
92
+ | `FIELD_AUDIT_AUDITORS` | A custom list of auditors for acquiring `change_context` info. | `["field_audit.auditors.RequestAuditor", "field_audit.auditors.SystemUserAuditor"]`
93
+ | `FIELD_AUDIT_SERVICE_CLASS` | A custom service class for audit logic implementation. | `field_audit.services.AuditService`
94
+
95
+ ### Custom Audit Service
96
+
97
+ The audit logic has been extracted into a separate `AuditService` class to improve separation of concerns and enable easier customization of audit behavior. Users can provide custom audit implementations by subclassing `AuditService` and configuring the `FIELD_AUDIT_SERVICE_CLASS` setting.
98
+
99
+ #### Creating a Custom Audit Service
100
+
101
+ ```python
102
+ # myapp/audit.py
103
+
104
+ from field_audit import AuditService
105
+
106
+ class CustomAuditService(AuditService):
107
+ def get_field_value(self, instance, field_name, bootstrap=False):
108
+ # Custom logic for extracting field values
109
+ value = super().get_field_value(instance, field_name, bootstrap)
110
+
111
+ # Example: custom serialization or transformation
112
+ if field_name == 'sensitive_field':
113
+ value = '[REDACTED]'
114
+
115
+ return value
116
+ ```
117
+
118
+ Then configure it in your Django settings:
119
+
120
+ ```python
121
+ # settings.py
122
+
123
+ FIELD_AUDIT_SERVICE_CLASS = 'myapp.audit.CustomAuditService'
124
+ ```
125
+
126
+ #### Backward Compatibility
127
+
128
+ The original `AuditEvent` class methods are maintained for backward compatibility but are now deprecated in favor of the service-based approach. These methods will issue deprecation warnings and delegate to the configured audit service.
129
+
130
+ ### Model Auditing
131
+
132
+ To begin auditing Django models, import the `field_audit.audit_fields` decorator
133
+ and decorate models specifying which fields should be audited for changes.
134
+ Example code:
135
+
136
+ ```python
137
+ # flight/models.py
138
+
139
+ from django.db import models
140
+ from field_audit import audit_fields
141
+
142
+
143
+ @audit_fields("tail_number", "make_model", "operated_by")
144
+ class Aircraft(models.Model):
145
+ id = AutoField(primary_key=True)
146
+ tail_number = models.CharField(max_length=32, unique=True)
147
+ make_model = models.CharField(max_length=64)
148
+ operated_by = models.CharField(max_length=64)
149
+ ```
150
+
151
+ #### Audited DB write operations
152
+
153
+ By default, Model and QuerySet methods are audited, with the exception of four
154
+ "special" QuerySet methods:
155
+
156
+ | DB Write Method | Audited
157
+ |:------------------------------|:-------
158
+ | `Model.delete()` | Yes
159
+ | `Model.save()` | Yes
160
+ | `QuerySet.bulk_create()` | No
161
+ | `QuerySet.bulk_update()` | No
162
+ | `QuerySet.create()` | Yes (via `Model.save()`)
163
+ | `QuerySet.delete()` | No
164
+ | `QuerySet.get_or_create()` | Yes (via `QuerySet.create()`)
165
+ | `QuerySet.update()` | No
166
+ | `QuerySet.update_or_create()` | Yes (via `QuerySet.get_or_create()` and `Model.save()`)
167
+
168
+ #### Auditing Special QuerySet Writes
169
+
170
+ Auditing for the four "special" QuerySet methods that perform DB writes (labeled
171
+ **No** in the table above) _can_ be enabled. This requires three extra usage
172
+ details:
173
+
174
+ > **Warning**
175
+ > Enabling auditing on these QuerySet methods might have significant
176
+ > performance implications, especially on large datasets, since audit events are
177
+ > constructed in memory and bulk written to the database.
178
+
179
+ 1. Enable the feature by calling the audit decorator specifying
180
+ `@audit_fields(..., audit_special_queryset_writes=True)`.
181
+ 2. Configure the model class so its default manager is an instance of
182
+ `field_audit.models.AuditingManager`.
183
+ 3. All calls to the four "special" QuerySet write methods require an extra
184
+ `audit_action` keyword argument whose value is one of:
185
+ - `field_audit.models.AuditAction.AUDIT`
186
+ - `field_audit.models.AuditAction.IGNORE`
187
+
188
+ ##### Important Notes
189
+
190
+ - Specifying `audit_special_queryset_writes=True` (step **1** above) without
191
+ setting the default manager to an instance of `AuditingManager` (step **2**
192
+ above) will raise an exception when the model class is evaluated.
193
+ - At this time, `QuerySet.delete()`, `QuerySet.update()`,
194
+ and `QuerySet.bulk_create()` "special" write methods can actually perform
195
+ change auditing when called with `audit_action=AuditAction.AUDIT`.
196
+ `QuerySet.bulk_update()` is not currently implemented and will raise
197
+ `NotImplementedError` if called with that action. Implementing this remaining
198
+ method remains a task for the future, see **TODO** below. All four methods do
199
+ support `audit_action=AuditAction.IGNORE` usage, however.
200
+ - All audited methods use transactions to ensure changes to audited models
201
+ are only committed to the database if audit events are successfully created
202
+ and saved as well.
203
+
204
+ ### Auditing Many-to-Many fields
205
+
206
+ Many-to-Many field changes are automatically audited through Django signals when
207
+ included in the `@audit_fields` decorator. Changes to M2M relationships generate
208
+ audit events immediately without requiring `save()` calls.
209
+
210
+ ```python
211
+ # Example model with audited M2M field
212
+ @audit_fields("name", "title", "certifications")
213
+ class CrewMember(models.Model):
214
+ name = models.CharField(max_length=256)
215
+ title = models.CharField(max_length=64)
216
+ certifications = models.ManyToManyField('Certification', blank=True)
217
+ ```
218
+
219
+ #### Supported M2M operations
220
+
221
+ All standard M2M operations create audit events:
222
+
223
+ ```python
224
+ crew_member = CrewMember.objects.create(name='Test Pilot', title='Captain')
225
+ cert1 = Certification.objects.create(name='PPL', certification_type='Private')
226
+
227
+ crew_member.certifications.add(cert1) # Creates audit event
228
+ crew_member.certifications.remove(cert1) # Creates audit event
229
+ crew_member.certifications.set([cert1]) # Creates audit event
230
+ crew_member.certifications.clear() # Creates audit event
231
+ ```
232
+
233
+ #### M2M audit event structure
234
+
235
+ M2M changes use specific delta structures in audit events:
236
+
237
+ - **Add**: `{'certifications': {'add': [1, 2]}}`
238
+ - **Remove**: `{'certifications': {'remove': [2]}}`
239
+ - **Clear**: `{'certifications': {'remove': [1, 2]}}`
240
+ - **Create** / **Bootstrap**: `{'certifications': {'new': []}}`
241
+
242
+ #### Bootstrap events for models with existing records
243
+
244
+ In the scenario where auditing is enabled for a model with existing data, it can
245
+ be valuable to generate "bootstrap" audit events for all of the existing model
246
+ records in order to ensure that there is at least one audit event record for
247
+ every model instance that currently exists. There is a migration utility for
248
+ performing this bootstrap operation. Example code:
249
+
250
+ ```python
251
+ # flight/migrations/0002_bootstrap_aircarft_auditing.py
252
+
253
+ from django.db import migrations, models
254
+ from field_audit.utils import run_bootstrap
255
+
256
+ from flight.models import Aircraft
257
+
258
+
259
+ class Migration(migrations.Migration):
260
+
261
+ dependencies = [
262
+ ('flight', '0001_initial'),
263
+ ]
264
+
265
+ operations = [
266
+ run_bootstrap(Aircraft, ["tail_number", "make_model", "operated_by"])
267
+ ]
268
+ ```
269
+
270
+ ##### Bootstrap events via management command
271
+
272
+ If bootstrapping is not suitable during migrations, there is a management command for
273
+ performing the same operation. The management command does not accept arbitrary
274
+ field names for bootstrap records, and uses the fields configured by the
275
+ existing `audit_fields(...)` decorator on the model. Example (analogous to
276
+ migration action shown above):
277
+
278
+ ```sh
279
+ manage.py bootstrap_field_audit_events init Aircraft
280
+ ```
281
+
282
+ Additionally, if a post-migration bootstrap "top up" action is needed, the
283
+ the management command can also perform this action. A "top up" operation
284
+ creates bootstrap audit events for any existing model records which do not have
285
+ a "create" or "bootstrap" `AuditEvent` record. Note that the management command
286
+ is currently the only way to "top up" bootstrap audit events. Example:
287
+
288
+ ```sh
289
+ manage.py bootstrap_field_audit_events top-up Aircraft
290
+ ```
291
+
292
+ ### Using with SQLite
293
+
294
+ This app uses Django's `JSONField` which means if you intend to use the app with
295
+ a SQLite database, the SQLite `JSON1` extension is required. If your system's
296
+ Python `sqlite3` library doesn't ship with this extension enabled, see
297
+ [this article](https://code.djangoproject.com/wiki/JSON1Extension) for details
298
+ on how to enable it.
299
+
300
+
301
+ ## Contributing
302
+
303
+ All feature and bug contributions are expected to be covered by tests.
304
+
305
+ ### Setup for developers
306
+
307
+ This project uses [uv](https://docs.astral.sh/uv/) for dependency management. Install uv and then install the project dependencies:
308
+
309
+ ```shell
310
+ cd django-field-audit
311
+ uv sync
312
+ ```
313
+
314
+ ### Running tests
315
+
316
+ **Note**: By default, local tests use an in-memory SQLite database. Ensure that
317
+ your local Python's `sqlite3` library ships with the `JSON1` extension enabled
318
+ (see [Using with SQLite](#using-with-sqlite)).
319
+
320
+ - Tests
321
+ ```shell
322
+ uv run pytest
323
+ ```
324
+
325
+ - Style check
326
+ ```shell
327
+ ruff check
328
+ ```
329
+
330
+ - Coverage
331
+ ```shell
332
+ uv run coverage run -m pytest
333
+ uv run coverage report -m
334
+ ```
335
+
336
+ ### Adding migrations
337
+
338
+ The example `manage.py` is available for making new migrations.
339
+
340
+ ```shell
341
+ uv run python example/manage.py makemigrations field_audit
342
+ ```
343
+
344
+ ### Publishing a new version to PyPI
345
+
346
+ Push a new tag to Github using the format vX.Y.Z where X.Y.Z matches the version
347
+ in [`__init__.py`](field_audit/__init__.py). Also ensure that the changelog is up to date.
348
+
349
+ Publishing is automated with [Github Actions](.github/workflows/pypi.yml).
350
+
351
+ ## TODO
352
+
353
+ - Implement auditing for the remaining "special" QuerySet write operations:
354
+ - `bulk_update()`
355
+ - Write full library documentation using github.io.
356
+
357
+ ### Backlog
358
+
359
+ - Add to optimization for `instance.save(save_fields=[...])` [maybe].
360
+ - Support adding new audit fields on the same model at different times (instead
361
+ of raising `AlreadyAudited`) [maybe].
362
+
@@ -0,0 +1,20 @@
1
+ field_audit/__init__.py,sha256=3_ZnH0SaqrZCTiCJjHrE8ELZlIoot1h4OJ5gEqIaQRk,143
2
+ field_audit/apps.py,sha256=04NYTi54zEuYPAhxEvsS61hmoPMIMVfPQKi4DOa9nE0,265
3
+ field_audit/auditors.py,sha256=V5Ese7w4V_WTLklIm5EOsjOJ3t4Iwp69AmgwxNXD33Y,4292
4
+ field_audit/const.py,sha256=U7c5Y4a5YjmvaZjsUpDhdTOvkcoaS5aqFf_L3C5GtYk,1135
5
+ field_audit/field_audit.py,sha256=lTEjaQFD1U8bVa6O-67wvbFY4riRcayc_4kCNjRFgao,8895
6
+ field_audit/middleware.py,sha256=JQMdM1vITddHFCqIX_M8_UDIvbCKU8BhY_sWccGVS-Y,468
7
+ field_audit/models.py,sha256=z7-Srvg7pjmyz_g3NrRosPNsvtbPNy17Ec1vHOXXp48,32514
8
+ field_audit/services.py,sha256=x_xoqmlKmGN4KD_LPsuUuoFJVmrgkwdugS_tgV6rUbM,16393
9
+ field_audit/utils.py,sha256=Jhal9wjUjZcQJsrXrOQoAxaIl62EhJbkfn2xPhiOBAM,3483
10
+ field_audit/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ field_audit/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ field_audit/management/commands/bootstrap_field_audit_events.py,sha256=0onszv7o3J03Y-NGjp8-74YVVtdyJkzcOUIJ6hs3hmQ,4327
13
+ field_audit/migrations/0001_initial.py,sha256=UXnmSkG8ZZmh-4DrqYACH9LSJ0Mh74nW0u_yKdS1ctg,1297
14
+ field_audit/migrations/0002_add_is_bootstrap_column.py,sha256=lkPNMk9Kb6IeaX9E08Snar-0_zr1epVrW9VSq24f9Ns,972
15
+ field_audit/migrations/0003_alter_auditevent_change_context_and_more.py,sha256=0MxLTzKAGCsIc6674-RZc6wGImbHpy5D069fTPbCjak,933
16
+ field_audit/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ django_field_audit-1.4.0.dist-info/licenses/LICENSE,sha256=DUmNjtND8byIKaTpjoksSUVazUJ_-3sPWOyyLiOvyDs,1508
18
+ django_field_audit-1.4.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
19
+ django_field_audit-1.4.0.dist-info/METADATA,sha256=m9ZPI1oBrdeWrvg4YwcfR5OTu0ETldVlYt6Zx_TIb2U,13035
20
+ django_field_audit-1.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2022, Dimagi Inc., and individual contributors.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name Dimagi, nor the names of its contributors, may be used
12
+ to endorse or promote products derived from this software without
13
+ specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL DIMAGI INC. BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,4 @@
1
+ from .field_audit import audit_fields # noqa: F401
2
+ from .services import AuditService, get_audit_service # noqa: F401
3
+
4
+ __version__ = "1.4.0"
field_audit/apps.py ADDED
@@ -0,0 +1,11 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class FieldAuditConfig(AppConfig):
5
+
6
+ name = "field_audit"
7
+ default_auto_field = "django.db.models.BigAutoField"
8
+
9
+ def ready(self):
10
+ from .auditors import audit_dispatcher
11
+ audit_dispatcher.setup_auditors()
@@ -0,0 +1,125 @@
1
+ from getpass import getuser
2
+ from subprocess import DEVNULL, CalledProcessError, check_output
3
+
4
+ from .models import (
5
+ USER_TYPE_PROCESS,
6
+ USER_TYPE_REQUEST,
7
+ USER_TYPE_TTY,
8
+ )
9
+ from .utils import class_import_helper
10
+
11
+ __all__ = [
12
+ "BaseAuditor",
13
+ "RequestAuditor",
14
+ "SystemUserAuditor",
15
+ "audit_dispatcher",
16
+ ]
17
+
18
+
19
+ class _AuditDispatcher:
20
+ """Dispatcher for the Audit API used to get "changed by" information when
21
+ creating new ``AuditEvent`` records (i.e. when an audited model is changed).
22
+
23
+ An instance of this class maintains the auditor chain that defines which
24
+ (and in what order) ``BaseAuditor`` subclass instances are used to acquire
25
+ "changed by" information.
26
+
27
+ The auditor chain can be customized by defining a list of 'BaseAuditor'
28
+ subclass paths via the ``FIELD_AUDIT_AUDITORS`` settings attribute.
29
+ """
30
+
31
+ def setup_auditors(self):
32
+ """Populate the auditors chain, possibly defined in settings.
33
+
34
+ This method is called at app ready time, and must be called before the
35
+ ``dispatch()`` method can be used.
36
+ """
37
+ from django.conf import settings
38
+ auditors_attr = "FIELD_AUDIT_AUDITORS"
39
+ if hasattr(settings, auditors_attr):
40
+ self.auditors = []
41
+ for auditor_path in getattr(settings, auditors_attr):
42
+ auditor_class = class_import_helper(
43
+ auditor_path,
44
+ f"{auditors_attr!r} item",
45
+ BaseAuditor,
46
+ )
47
+ self.auditors.append(auditor_class())
48
+ else:
49
+ self.auditors = [RequestAuditor(), SystemUserAuditor()]
50
+
51
+ def dispatch(self, request):
52
+ """Cycles through the auditors chain and returns the first non-None
53
+ value returned by a call to ``auditor.change_context(request)``.
54
+
55
+ :param request: Django request to be audited (or ``None``).
56
+ :returns: JSON-serializable value (or ``None`` if chain is exhausted).
57
+ """
58
+ for auditor in self.auditors:
59
+ change_context = auditor.change_context(request)
60
+ if change_context is not None:
61
+ return change_context
62
+ return None
63
+
64
+
65
+ audit_dispatcher = _AuditDispatcher()
66
+
67
+
68
+ class BaseAuditor:
69
+ """Abstract class for the Auditor API. Subclasses are used to return
70
+ "changed by" information associated with an event which they know how to
71
+ audit.
72
+
73
+ BaseAuditor subclasses must define the following:
74
+ - ``change_context()`` method that returns a JSON-serializable object of
75
+ information for events it knows how to audit (or ``None`` otherwise).
76
+ """
77
+
78
+ def change_context(self, request):
79
+ raise NotImplementedError("change_context() is abstract")
80
+
81
+
82
+ class RequestAuditor(BaseAuditor):
83
+ """Auditor class for getting users from authenticated requests."""
84
+
85
+ def change_context(self, request):
86
+ if request is None:
87
+ # cannot provide a request user without a request
88
+ return None
89
+ if request.user.is_authenticated:
90
+ return {
91
+ "user_type": USER_TYPE_REQUEST,
92
+ "username": request.user.username,
93
+ }
94
+ # short-circuit the audit chain for not-None requests
95
+ return {}
96
+
97
+
98
+ class SystemUserAuditor(BaseAuditor):
99
+ """Auditor class for getting OS usernames."""
100
+
101
+ def __init__(self):
102
+ self.has_who_bin = True
103
+
104
+ def change_context(self, request):
105
+ username = None
106
+ if self.has_who_bin:
107
+ try:
108
+ # get owner of STDIN file on login sessions (e.g. SSH)
109
+ output = check_output(["who", "-m"], stderr=DEVNULL)
110
+ except (FileNotFoundError, CalledProcessError):
111
+ self.has_who_bin = False
112
+ else:
113
+ if output:
114
+ try:
115
+ username = output.split()[0].decode("utf-8")
116
+ user_type = USER_TYPE_TTY
117
+ except (IndexError, UnicodeDecodeError):
118
+ pass
119
+ if not username:
120
+ # no TTY user, get owner of the current process
121
+ username = getuser()
122
+ user_type = USER_TYPE_PROCESS
123
+ if username:
124
+ return {"user_type": user_type, "username": username}
125
+ return None
field_audit/const.py ADDED
@@ -0,0 +1,19 @@
1
+
2
+ # Number of records to bulk fetch/insert per batch for bootstrap operations.
3
+ #
4
+ # Benchmark testing of this value was performed by bootstrapping a table with
5
+ # five (5) columns and ~2.6 million rows. Two benchmark runs were performed with
6
+ # the database reset and restarted between runs. The first benchmark used
7
+ # 'batch_size=1000' and completed in 6min 15sec, the second benchmark used
8
+ # 'batch_size=10000' and completed in 6min 12sec (less than 1% difference in
9
+ # runtime). There was no noticeable difference in database resource usage
10
+ # between the two runs, but the Django process consistently used about 160MiB
11
+ # more memory for the duration of the second benchmark compared to the first.
12
+ # Given that the overall runtime was relatively unaffected between two tests
13
+ # whose batch size differed by an order of magnitude, the lower value seems like
14
+ # the better default due to the lower Django resource usage.
15
+ #
16
+ # Installations with noticeable database connection latency may prefer to
17
+ # specify a higher value on their bootstrap operations in order to optimize for
18
+ # fewer round-trips to the database.
19
+ BOOTSTRAP_BATCH_SIZE = 1000