Skip to content

Analyse Permission Audit Logs

This tutorial explains how to enable the permission audit logger, capture events, and analyse them for policy verification. It builds on the infrastructure provided in the audit logging API.

1. Enable logging in development

Choose an audit logger implementation. FileAuditLogger writes newline-delimited JSON, while DatabaseAuditLogger stores entries in a Django-managed table.

# settings.py
from general_manager.permission.audit import FileAuditLogger, configure_audit_logger


configure_audit_logger(
    FileAuditLogger(path="/var/log/general_manager/permission.log")
)

Alternatively, configure via Django settings:

# settings.py
GENERAL_MANAGER = {
    "AUDIT_LOGGER": {
        "class": "general_manager.permission.audit.DatabaseAuditLogger",
        "options": {"using": "default", "table_name": "gm_permission_log"},
    }
}

configure_audit_logger_from_settings() resolves those settings during app startup. GENERAL_MANAGER["AUDIT_LOGGER"] takes precedence over a top-level AUDIT_LOGGER; if neither is present, logging is disabled by installing the no-op logger.

2. Trigger and inspect events

Each call to BasePermission.check_* or mutation permissions emits a PermissionAuditEvent when logging is active. Entries contain:

Field Meaning
timestamp Recorded when the event is serialised.
action "create", "read", "update", "delete", or "mutation".
attributes Tuple of attribute names that were evaluated.
granted True when the check passed.
bypassed True when the request user was a superuser.
manager Name of the manager class, if known.
user_id Primary key of the user when available.
user_repr repr(user) when no primary key is available.
permissions All expressions evaluated, including inherited rules from __based_on__.
metadata JSON-compatible custom context.

Load events into your preferred analysis tool:

jq '. | select(.action == "update" and (.granted == false))' permission.log

For database logging, run Django ORM queries:

from general_manager.permission.audit import DatabaseAuditLogger

logger = DatabaseAuditLogger(table_name="gm_permission_log", using="default")
denied = logger.model.objects.filter(granted=False).order_by("-created_at")

3. Build dashboards and alerts

  1. Aggregate denied events by manager and attributes to spot missing rules.
  2. Track bypasses (bypassed=True) to ensure superuser usage stays intentional.
  3. Correlate with request metadata by attaching extra data via the metadata field. When calling permission checks manually, pass context:
emit_permission_audit_event(
    PermissionAuditEvent(
        action="update",
        attributes=("status",),
        granted=False,
        user=request_user,
        manager="Project",
        permissions=("isSelf",),
        metadata={"request_id": request_id},
    )
)
  1. Automate regression detection by running a nightly query that compares denied counts across deployments.

4. Test your logging setup

  • In unit tests, inject a stub logger implementing record() to capture events.
  • Assert that permission checks produce both success and failure entries.
  • Call flush() on buffered loggers (FileAuditLogger, and DatabaseAuditLogger on non-SQLite databases) whenever accepted events must be persisted while logging continues. Call close() only at the terminal lifecycle boundary: it rejects later record() calls, waits for accepted events to drain, and re-raises a worker persistence failure. Repeated close() calls return after a successful close and report the retained failure after an unsuccessful one. There is no close timeout, so a persistence operation that never returns keeps close() blocked rather than reporting false success.

With logging enabled and observed, you can prove your permission model behaves as intended and quickly diagnose unexpected access results.