API
Audit decorator
- alliance_platform.audit.create_audit_model_base(model, *, meta_base=<class 'object'>, manual_events=None, events=None, registry=<alliance_platform.audit.registry.AuditRegistry object>, list_perm=None, fields=None, exclude=None, related_name=Unset.token, **kwargs)
Given
model, returns a base to be used for creating a table to attach audit log to:class UserAuditEvent( create_audit_model_base(User, exclude=["password", "last_login"], manual_events=["LOGIN", "LOGOUT"]) ): class Meta: db_table = "xenopus_frog_user_auditevent"
Should you wish to add events that are manually-triggered eg a pdf file had been downloaded, just specify those events in
manual_events; to then trigger a manual event usecreate_audit_event().eventsrefers to theRowEventto be used which defaults toAuditSnapshot. This auditsCREATE,UPDATEandDELETEevents and has handling for many to many fields. You most likely do not need to change this option.If you need to monitor other database events, such as BeforeInsert, you could do so by passing in
events; this will supersede AuditSnapshot which if you intend to keep can be done by addingAuditSnapshot(label="your label")to the list. SeeAuditSnapshotto see what you need to be aware of before doing this.One noteworthy kwarg is
fields, where you can ask the audit module to only watch changes made to your selected list of fields. By default, all fields are tracked, and should be kept this way unless you have a good reason to change this behavior. If you wish to record all fields but only display some of them on frontend (eg, sensitive fields), then defineaudit_fields_to_displayon your model.FileField and ImageField are recorded as string (URL), AutoField is recorded as IntegerField and any ManyToManyField is recorded only on the sourcing side as an ArrayField with same field name (eg.
bug=ManyToManyField(Bug)onCoderwill result in an array of bug’s ids being recorded incoder.events.bug, but this field will not be created on the Bug model even if its also audited)See
pghistory.core.create_event_model()for the supportedkwargsand more detailsUsage:
class PDFAuditEvent(create_audit_model_base(PDFFile, manual_events=["accessed"])): audit_fields_to_display=['id', 'uploader', 'file'] class Meta: db_table = "pdf_file_audit_log" # You must pass the record the event is logged against. Acting user is also tracked (by default) # by AuditMiddleware and stored in context. create_audit_event(pdf, "accessed")
- Parameters:
model (type[AuditableModelProtocol]) – The model to create audit event model for
meta_base (type) – The base class for Meta, eg. NoDefaultPermissionsMeta
manual_events (list[str] | None) – List of manual events supported for this model
events (list[Tracker] | None) – Database events to monitor. See above comments for more details.
registry (AuditRegistry) – The audit registry to add to. You most likely don’t need this; the default suffices for most cases
list_perm (str | None) – The permission to use when showing audit events in list view. This should be a global permission (ie. doesn’t accept a specific object). If not specified uses
resolve_perm_name()with an action of ap_audit_settings.LIST_PERM_ACTION (which defaults toaudit) for the modelmodel(ie. the source model).fields (list[str] | None) – The fields to track. If None, all fields on
modelare tracked.exclude (list[str] | None) – Exclude these fields from tracking. Only one of
fieldsandexcludeshould be specified.related_name (str) – The primary way to identify the relation of the created model and the tracked model. If
fieldsorexcludeare not provided this defaults toauditeventsotherwise a name is generated based on the provided fields (seepghistory.core.create_event_model()).
- Return type:
type[Model]
- alliance_platform.audit.with_audit_model(meta_base=<class 'allianceutils.auth.permission.NoDefaultPermissionsMeta'>, audit_fields_to_display=None, **kwargs)
Model class decorator to create an associated audit model
Wraps
create_audit_model_base()in decorator form and adds some sensible defaults- Parameters:
meta_base (type)
audit_fields_to_display (Iterable[str] | None)
- Return type:
Callable[[type[Model]], Model]
Events
- class alliance_platform.audit.events.PatchedEvent(*, name=None, operation=None, condition=None, label=None, event_model=None, when=None, row=None, snapshot=None, level=None)[source]
Patched pghistory.trigger.event.
This fixes the issue as seen here: https://github.com/jyveapp/django-pghistory/issues/9 and also handles many-to-many (by copying values from the previous record if available).
Limitation: because we copy the m2m values from prev. record, the value is not Guaranteed to be correct: it will be most of times, but would be empty if this gets instlalled on an existing model and no changes to m2m had been made since.
- Parameters:
name (str | None)
operation (Operation | None)
condition (Condition | None)
event_model (AuditEventProtocol)
when (When | None)
level (Level | None)
- class alliance_platform.audit.events.AuditSnapshot(label=None)[source]
our Snapshot event. Audits AfterInsert, AfterUpdate and BeforeDelete. In the case of update, compares before<->after to see if any value gets modified; this means a
instance.save()without any actual changes will NOT trigger an UPDATE snapshot.This also writes a self-referencing
pgh_previous_idthat points to last previous record for the same object: effectively a pgh_previous=ForeignKey(‘self’, null=True) that can be used to find out what values have changed.Also audits many-to-many fields by placing triggers on the through table.
- Parameters:
label (str)
- alliance_platform.audit.events.create_event(obj, registration, *, label, using='default')[source]
Patched pghistory.core.create_event.
Dropped event registration check as it’s already done in create_audit_event, and also adds M2M fields handling support.
- alliance_platform.audit.create_audit_event(object, label)
Manually logs an event against a preset manual-event for object’s class, eg:
create_audit_event(pdf, "accessed").The event must be registered on the specified model (eg. should be passed in
manual_eventstocreate_audit_model_base()).All manual log entries are tied to objects (ie, you can’t have object-less events such as
create_audit_event("system shutdown")). By default,AuditMiddleware()tracks and records the current user and URL inpgh_contextfor the created log event, and additional info can be added by wrappingcreate_audit_eventinwith pghistory.context(**kwargs). Seepghistory.contextfor more details about how context works.Returns created event model.
- Parameters:
object (Model)
label (str)
- Return type:
Model
Views
- class alliance_platform.audit.api.AuditLogView(**kwargs)[source]
This viewset handles returning data from the appropriate model Event table based on the
modelrequest parameter. Ifmodel="all"then all audited models are returned using a database UNION.In most apps there will be a single
AuditRegistryand you never need to explicitly define it. In cases where multiple registries are desired (for example to split between different apps - admin vs public app) you can pass theregistryargument:path("api/auditlog/", AuditLogView.as_view(registry=my_registry))
If you wish to restrict the queryset for any Events in some way override
get_single_queryset().Note that in the case that all models are being shown
get_querysetwill return aEventUnioninstead of aQuerySet. Each queryset in theunion.querysetswill be filtered infilter_querysetbefore being combined with a.union()call. If you overrideget_querysetyou should handle this (eg. callsuper().get_queryset()) and handle the case where aEventUnionis returned. It is recommended you overrideget_single_queryset()orget_multiple_queryset()instead.All fields available on the source model will be available on on the queryset as well regardless of whether they exist in audit_fields_to_display; you can do something like
return qs.filter(owner=request.user.org)if all audited models has an “owner” attribute; or you could refine based on model:if qs.model==FooEvent: return qs.filter(bar=request.user)or if you want to refer to the original audited modelif qs.pgh_tracked_model.model==Foo: return qs.filter(bar=request.user)- get_single_queryset(qs)[source]
Called on each event queryset. You can override this to add filters you want applied to all event querysets. Note that adding annotations here won’t work when multiple event querysets are being returned. For advanced cases you will need to override
get_multiple_queryset.- Parameters:
qs (QuerySet)
- Return type:
QuerySet
- get_multiple_queryset()[source]
Called when
modelrequest param is ‘all’.Each model queryset is passed to
get_single_queryset.Filtering occurs in
filter_querysetand union is applied after filtering.- Return type:
- get_queryset()[source]
Get the queryset or EventUnion to use.
In the case of handling multiple models (when
modelis ‘all’) this will return an EventUnion.To override the handling of logic for each type of model override
get_single_queryset. This will be called byget_queryseton the single model or on each model when returning all models. You can add conditional logic there for specific models. Note that you cannot add annotations as they will not work with a union. If you need to support this overrideget_multiple_querysetas well.- Return type:
QuerySet | EventUnion
Registry
Search
- alliance_platform.audit.search.search_audit_by_context(search, created_between=(None, None), models=None)[source]
Given
search, returns all audit histories from all registered models with context matching the search dict.The search is partial:
{"user": 1}will return all entries with context value containinguser=1regardless of what other values may be set.A created_between can be passed in to restrict to only find contexts created between two datetime points; passing
Nonein either value results in the relevant side being open ended.You can also pass in models to restrict what kind of source models the search should look for, by default it searches across all registered models.
By default context is populated by
AuditMiddlewareand includesuser - the user (or hijacked user) who triggered the changed
hijacker - the original user in the case
useris hijackedurl - the current URL
ip - the users IP address (only if
TRACK_IP_ADDRESSis enabled)
Extra context can be added with pghistory.context.
For changes that occur outside of a request no context will be set unless explicitly wrapped in
pghistory.context.Returns a dict indexed by models with values being the queryset, e.g.
{PaymentRecord: qs}whereqsis a PaymentRecordAuditEvent queryset containing all hits, which can then be queried further depending on your need (e.g.qs.filter(payment_method="cash")). WARNING: all searched models are returned in the dict regardless of whether theirqs.count() == 0for performance reasons. Do not rely on keys of returned dict to directly decide which model contained a hit.usage:
# find all changes made to Favorite and PaymentRecord models by user 2 hijacking someone else # on /payments/ url before yesterday, regardless of who this user impersonated as search_audit_by_context( search={ 'hijacker': 2, 'url': '/payments/', }, created_between=[None, timezone.now()+timedelta(days=-1)], models=[Favorite, PaymentRecord] )
- Parameters:
search (dict[str, Any])
created_between (tuple[datetime | None, datetime | None])
models (list[type[Model]] | None)
- Return type:
dict[type[Model], type[QuerySet]]
Middleware
- class alliance_platform.audit.middleware.AuditMiddleware(get_response)[source]
Tracks POST/PUT/PATCH/DELETE requests and annotates a few fields in the pghistory context.
By default tracks user id, impersonatinguser id and url visited. IP address tracking can be turned on by enabling TRACK_IP_ADDRESS in audit package settings - make sure you take GDPR into consideration (recording without disclosure is a violation; ie. minimal: your site need to have a privacy statement somewhere.)