Skip to content

Keep an audit trail

Every change to access is a signal, sent once the change commits - so a receiver never records a change that was rolled back. Each carries actor, who made it, None for the system.

# audit/receivers.py
from django.dispatch import receiver

from clearance import signals
from audit.models import AccessChange


@receiver(signals.bundle_given)
def bundle_given(sender, actor, person, bundle, **kwargs):
    AccessChange.objects.create(actor=actor, person=person, what=f'given {bundle}')


@receiver(signals.permission_revoked)
def permission_revoked(sender, actor, person, permission, reason, **kwargs):
    AccessChange.objects.create(
        actor=actor, person=person, what=f'took {permission}: {reason}'
    )

Connect the receivers when the app starts:

class AuditConfig(AppConfig):
    name = 'audit'

    def ready(self):
        from audit import receivers  # noqa: F401

Every signal and what it carries is in the reference. A change made in the admin site, in code or by clearance_sync is the same signal: they all go through the same services.

What is not a signal

Who reads a record is not a change to access: record it where the record is read - a view, a serializer. A refusal is not a change either; it is an AccessRefused the caller catches, or a 403.