Skip to main content

pytest-authz-matrix

CI Python License: MIT

Authorization contract testing for Python web APIs.

pytest-authz-matrix expands one pytest test into the complete actor × resource-relationship matrix for an endpoint. For Django REST Framework projects, it can also inventory API routes and report which HTTP method/route pairs do not have an authorization contract.

It is built for the bugs that ordinary authentication tests miss:

  • a user retrieves another user's object by changing an ID;
  • a tenant administrator reaches an object belonging to another tenant;
  • a list endpoint leaks foreign rows while its detail endpoint is protected;
  • an endpoint returns 403 when policy requires concealing the object's existence with 404;
  • a new DRF route ships without any ownership or cross-tenant test.

Status: 0.1.0 is an alpha focused on explicit DRF authorization contracts. The plugin discovers untested routes; it deliberately does not guess your business authorization policy.

Quick start

Install the plugin with its DRF integration:

pip install "pytest-authz-matrix[django]"

Create authz-matrix.yml in the pytest root:

version: 1

actors:
  owner: owner_client
  same_tenant_user: same_tenant_client
  foreign_tenant_user: foreign_tenant_client
  anonymous: anonymous_client

resources:
  booking:
    fixture: booking_matrix
    lookup: pk

contracts:
  booking.retrieve:
    method: GET
    path: /api/bookings/{resource}/
    route_name: booking-detail
    resource: booking
    matrix:
      owner:
        owned: allow
        same_tenant: conceal
        foreign_tenant: conceal
      same_tenant_user:
        owned: conceal
        same_tenant: allow
        foreign_tenant: conceal
      foreign_tenant_user:
        owned: conceal
        same_tenant: conceal
        foreign_tenant: allow
      anonymous:
        owned: unauthenticated
        same_tenant: unauthenticated
        foreign_tenant: unauthenticated

The actor fixtures return the API clients that already know how your project authenticates:

import pytest
from rest_framework.test import APIClient


@pytest.fixture
def owner_client(owner):
    client = APIClient()
    client.force_authenticate(owner)
    return client


@pytest.fixture
def same_tenant_client(same_tenant_user):
    client = APIClient()
    client.force_authenticate(same_tenant_user)
    return client


@pytest.fixture
def foreign_tenant_client(foreign_tenant_user):
    client = APIClient()
    client.force_authenticate(foreign_tenant_user)
    return client


@pytest.fixture
def anonymous_client():
    return APIClient()

The resource fixture maps the relationship names in YAML to real model instances:

@pytest.fixture
def booking_matrix(owner_booking, same_tenant_booking, foreign_tenant_booking):
    return {
        "owned": owner_booking,
        "same_tenant": same_tenant_booking,
        "foreign_tenant": foreign_tenant_booking,
    }

Finally, bind a test to the contract:

import pytest


@pytest.mark.authz_contract("booking.retrieve")
def test_booking_retrieve_authorization(authz_case):
    authz_case.run()

That single function becomes 12 independent pytest cases with readable IDs such as:

booking.retrieve[owner-owned-allow]
booking.retrieve[owner-foreign_tenant-conceal]
booking.retrieve[anonymous-owned-unauthenticated]

Outcomes

The built-in outcomes are HTTP status contracts:

Outcome Default status Meaning
allow 200 The actor may perform the operation.
deny 403 The actor is authenticated but forbidden.
conceal 404 The resource's existence must not be disclosed.
unauthenticated 401 Authentication is required.

Override defaults globally when an endpoint legitimately returns another success status:

outcomes:
  allow: [200, 201, 204]
  deny: [403]
  conceal: [404]
  unauthenticated: [401]

Or override one matrix cell:

matrix:
  owner:
    owned:
      outcome: allow
      statuses: [200, 204]

An integer or list is also accepted for a status-only expectation:

matrix:
  owner:
    owned: [200, 206]

Mutating endpoints

Request bodies and query strings can come from fixtures:

contracts:
  booking.update:
    method: PATCH
    path: /api/bookings/{resource}/
    route_name: booking-detail
    resource: booking
    request:
      data_fixture: booking_update_payload
      query_fixture: update_query
      format: json
      headers:
        X-Test-Source: authz-matrix
    matrix:
      owner:
        owned: allow
        foreign_tenant: conceal

For state or side-effect assertions, split execution from the status assertion:

@pytest.mark.authz_contract("booking.update")
def test_booking_update_authorization(authz_case):
    original_status = authz_case.resource.status

    response = authz_case.execute()

    authz_case.resource.refresh_from_db()
    if authz_case.outcome != "allow":
        assert authz_case.resource.status == original_status
    authz_case.assert_response(response)

authz_case.execute() also accepts per-test data, query, and headers overrides.

Route coverage

Add route_name to contracts whenever possible. The plugin matches the contract's HTTP method and Django URL name against DRF's URL resolver. Without a route name, it falls back to normalized path matching.

pytest --authz-report

Example output:

============================= authorization matrix =============================
authorization cases: 12/12 asserted, 12 passed, 0 failed
authorization contracts: 1/1 exercised
DRF route coverage: 9/11 (81.8%)
  missing: PATCH children-detail
  missing: POST booking-refund

Fail CI when route coverage drops below a threshold:

pytest --authz-report --authz-fail-under=85

Write machine-readable results:

pytest --authz-report-json=build/authz-report.json

Discovery is best-effort and only runs when Django is configured in the pytest session. A plain Python or non-Django test suite can still use explicit matrices with any client fixture exposing HTTP method functions such as .get() or .patch().

Path templates

Given a resource fixture object, the following placeholders are available:

Placeholder Resolution
{resource} The resource field configured by lookup (pk by default).
{resource.uuid} Any mapping key or object attribute on the selected resource.
{params.estate} A static value from the contract's params mapping.

Values are URL-encoded before insertion. See the full configuration reference for endpoint-only contracts, request options, and validation rules.

What this version does not do

  • It does not infer who should own an object. Fixtures define that truth explicitly.
  • It does not prove that response bodies contain no foreign objects; add a list-response assertion.
  • It does not intercept emails, Celery tasks, or external API calls automatically.
  • It does not yet generate contracts from OpenAPI or support a first-class FastAPI adapter.
  • It does not replace database row-level-security tests or a security review.

These boundaries are intentional. The first release makes authorization policy executable and shows what remains untested without claiming to solve authorization automatically.

Development

python -m venv .venv
. .venv/bin/activate
pip install -e ".[dev]"
pytest
ruff check src tests
mypy src
python -m build

See CONTRIBUTING.md before opening a pull request. The architecture and planned extension points are documented in docs/design.md.

License

MIT

Release files for pytest-authz-matrix 0.1.0

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

Source distribution (sdist)

Source distribution for pytest-authz-matrix 0.1.0
File Size Uploaded
pytest_authz_matrix-0.1.0.tar.gz 22.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-authz-matrix 0.1.0
File Interpreter ABI Platform
pytest_authz_matrix-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 41.4 kB

Release files / pytest_authz_matrix-0.1.0.tar.gz

Download URL pytest_authz_matrix-0.1.0.tar.gz
Size 22.2 kB
Tags Source
SHA-256 checksum
How to use checksums
3144d662cd5ba871c74573df0fd2cc3b02bce0d2c8ae6638a22794a4bc74f2bb
BLAKE2b-256 checksum
How to use checksums
3114d2bcb0403aecb5fc5ae62dc840539f7f9240d8a5e3c4ec0cf801366803d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.

Transparency log

Release files / pytest_authz_matrix-0.1.0-py3-none-any.whl

Download URL pytest_authz_matrix-0.1.0-py3-none-any.whl
Size 19.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7215682c8190b16156ac83b9e4bcb2ff5e105358ef46453b119c69671b394d5a
BLAKE2b-256 checksum
How to use checksums
beff23898338f2e62c42d1c9f3b649293ac3cfc37b21039d2898513f79e98f15
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.1

2 release files

This release

0.1.0 This release

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