Skip to main content

Django Permission Tracer

🔍 Analyze and visualize permissions across your Django REST Framework app.

See every endpoint with the permissions that guard it, find everywhere a permission is used, and catch endpoints that are open to anyone.

Django REST Framework permissions are spread across permission_classes, @action(...) overrides, get_permissions() methods, composed expressions like IsAuthenticated | IsOwner, and global defaults. Permission Tracer resolves all of that for you, both statically (for every endpoint and HTTP method) and at runtime (for each request).

Features

  • 🎯 Endpoint → permissions, per HTTP method and viewset action, including @action overrides, get_permissions() overrides and composed permissions (&, |, ~)
  • 🔄 Permission → endpoints reverse lookup
  • 🐛 Request tracing: for each request, every permission's result, which one denied it, and for composed permissions which operand failed. Covers both has_permission and has_object_permission
  • 🚪 Anonymous-access audit: flags endpoints an unauthenticated user can reach; use it in CI with --fail-on-unprotected
  • 📝 Permission matrix export as Markdown or CSV, for docs, PRs and security reviews
  • 📊 Web dashboard with search and a graph view

Screenshot

Permission Tracer dashboard overview

Endpoints and permissions in the dashboard

Graph view of a permission and its endpoints

Quickstart

pip install "django-permission-tracer[drf]"
# settings.py
INSTALLED_APPS = [
    # ...
    "permission_tracer",
]

MIDDLEWARE = [
    # ...
    "permission_tracer.middleware.PermissionTracerMiddleware",
]
# urls.py
urlpatterns = [
    # ...
    path("_permission-tracer/", include("permission_tracer.urls")),
]

Run the dev server (DEBUG = True), log in as a staff user (for example through /admin/) and open http://localhost:8000/_permission-tracer/. You can mount it at any prefix you like.

Safe by default: the tracer is only enabled when DEBUG = True, and the dashboard and API are restricted to active staff users. It shows your permission classes' source code and recent requests, so keep it that way in any shared environment.

To change who can open it, set ACCESS_CHECK. For local development, this lets anyone in while DEBUG = True:

PERMISSION_TRACER = {"ACCESS_CHECK": "permission_tracer.conf.allow_in_debug"}

See Configuration for all options.

Usage

Debug a denied request

Make the request, then open the Traces tab (or GET <prefix>/api/trace/?denied=1). Each trace shows:

  • the view, viewset action and user (and which authenticator authenticated them)
  • every permission check, at view level and object level, and whether it passed
  • the permission that denied the request, with DRF's error message
  • for composed permissions, which operand failed, e.g. ✗ OR → ✗ IsAuthenticated, ✗ IsOwner

Permission report and matrix

python manage.py permission_tracer_analyze                     # readable report
python manage.py permission_tracer_analyze --format markdown   # matrix for docs / PR descriptions
python manage.py permission_tracer_analyze --format csv --output permissions.csv
python manage.py permission_tracer_analyze --format json

Example Markdown output:

Method Path Action Permissions Anonymous
GET /api/articles/ list IsAuthenticated | IsOwner no
GET /api/articles/public/ public AllowAny yes
POST /api/articles/{pk}/publish/ publish IsAdminUser no

"Anonymous" is worked out by calling each permission with an unauthenticated request: yes, no, or ? if a permission raised an error.

Fail CI when an endpoint is accidentally public

python manage.py permission_tracer_analyze --format markdown \
    --fail-on-unprotected \
    --allow /api/ \
    --allow '/api/auth/*' \
    --allow 'GET /api/articles/*'

The command exits non-zero and lists every endpoint method that anonymous users can reach, except those matching an --allow pattern (a glob, optionally prefixed with an HTTP method).

Configuration

All settings are optional:

PERMISSION_TRACER = {
    # None (default) follows settings.DEBUG. Set True to force it on, e.g. on a staging server.
    "ENABLED": None,
    # Who may open the dashboard/API: a callable or dotted path taking the request.
    # Default: active staff users. 'permission_tracer.conf.allow_in_debug' lets anyone in when DEBUG=True.
    "ACCESS_CHECK": "permission_tracer.conf.staff_only",
    # 'memory': per-process (fine for runserver). 'cache': uses Django's cache, shared across workers.
    "STORAGE_BACKEND": "memory",
    "MAX_TRACES": 100,
    "TRACE_TIMEOUT": 3600,  # seconds, 'cache' backend only
    "EXCLUDE_PATHS": [
        "/admin/",
        "/static/",
        "/media/",
    ],  # the tracer's own URLs are always excluded
}

Projects with token or JWT authentication middleware

If your project authenticates in a middleware that rejects any request without a token, opening the dashboard in a browser returns that middleware's error (for example 401 Authentication failed, access token is required). A browser tab can't send your API's token headers.

Wrap that middleware with tracer_exempt. The wrapped version lets the tracer's own URLs through while the tracer is enabled, and behaves exactly as before for every other request:

# myproject/middleware.py
from permission_tracer.middleware import tracer_exempt

from myproject.auth import JWTAuthMiddleware

TracerExemptJWTAuthMiddleware = tracer_exempt(JWTAuthMiddleware)
# settings.py: swap it in, same position in MIDDLEWARE
MIDDLEWARE = [
    # "myproject.auth.JWTAuthMiddleware",
    "myproject.middleware.TracerExemptJWTAuthMiddleware",
    # ...
]

These projects usually have no Django session login either, so the default staff-only ACCESS_CHECK can't recognize anyone. For local development, use:

PERMISSION_TRACER = {"ACCESS_CHECK": "permission_tracer.conf.allow_in_debug"}

Without the dashboard, python manage.py permission_tracer_analyze gives you the same endpoint and permission report on the command line, and needs no browser access at all.

How it works

  • Static analysis walks your URLconf. For every DRF route it builds the view the same way DRF's router would (same initkwargs and action map) and calls get_permissions() once per HTTP method. The result is what DRF would actually use, not just what the permission_classes attribute says. Django class-based views report LoginRequiredMixin / PermissionRequiredMixin / UserPassesTestMixin. Plain function views without DRF aren't covered.
  • Runtime tracing wraps APIView.check_permissions and APIView.check_object_permissions once at startup. Each request's trace lives in a contextvars.ContextVar, so it is safe under threaded and async servers. Outside a traced request the wrappers just call DRF's original methods. Views that override check_permissions themselves are not traced.

Development

pip install -e ".[drf]" pytest pytest-django
pytest

See CONTRIBUTING.md for guidelines.

License

MIT License - see LICENSE file for details.

Metadata

Release files for django-permission-tracer 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-permission-tracer 0.1.2
File Size Uploaded
django_permission_tracer-0.1.2.tar.gz 42.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-permission-tracer 0.1.2
File Interpreter ABI Platform
django_permission_tracer-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 80.8 kB

Release files / django_permission_tracer-0.1.2.tar.gz

Download URL django_permission_tracer-0.1.2.tar.gz
Size 42.3 kB
Tags Source
SHA-256 checksum
How to use checksums
81634930219bbdf850ca67210143f1b953dfae090cc75c4af232d8c3fd793ceb
BLAKE2b-256 checksum
How to use checksums
e53fca04c06dcbcee4e104d03d3c3150089f41b56caeb2d1324afe343c7f80f4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / django_permission_tracer-0.1.2-py3-none-any.whl

Download URL django_permission_tracer-0.1.2-py3-none-any.whl
Size 38.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e625a98784384caedc600874402c1eec429cf19599841283c7ee62090b4e6b95
BLAKE2b-256 checksum
How to use checksums
fe858b8c4976b40097284ad796b91bb44930698b5e7fe4dd8b11923426f5cd7d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

0.1.3

2 release files

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page