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.
- django_field_audit-1.4.0.dist-info/METADATA +362 -0
- django_field_audit-1.4.0.dist-info/RECORD +20 -0
- django_field_audit-1.4.0.dist-info/WHEEL +4 -0
- django_field_audit-1.4.0.dist-info/licenses/LICENSE +24 -0
- field_audit/__init__.py +4 -0
- field_audit/apps.py +11 -0
- field_audit/auditors.py +125 -0
- field_audit/const.py +19 -0
- field_audit/field_audit.py +255 -0
- field_audit/management/__init__.py +0 -0
- field_audit/management/commands/__init__.py +0 -0
- field_audit/management/commands/bootstrap_field_audit_events.py +127 -0
- field_audit/middleware.py +17 -0
- field_audit/migrations/0001_initial.py +37 -0
- field_audit/migrations/0002_add_is_bootstrap_column.py +31 -0
- field_audit/migrations/0003_alter_auditevent_change_context_and_more.py +32 -0
- field_audit/migrations/__init__.py +0 -0
- field_audit/models.py +824 -0
- field_audit/services.py +378 -0
- field_audit/utils.py +91 -0
|
@@ -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,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.
|
field_audit/__init__.py
ADDED
field_audit/apps.py
ADDED
field_audit/auditors.py
ADDED
|
@@ -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
|