Skip to main content

PermissionDiff

Prove your code didn't just hand the wrong person the keys.

CI Python License Ruff Typed

PermissionDiff is a CI-native tool that shows exactly how a code change alters effective authorization. It answers: “Did this pull request accidentally give a user, tenant, role, or service access it did not have before?”

PermissionDiff generates meaningful authorization cases from subjects, resources, actions, and context values you declare. It checks explicit invariants and records baseline cases so a candidate authorizer evaluates the same exact corpus. Security verdicts are deterministic Python decisions—never LLM judgments.

CRITICAL — Cross-tenant access must be denied
support(acme) → read_invoice → invoice(globex)
actual: ALLOW
repro: .permissiondiff/failures/PD-0001.json

PermissionDiff is at v0/alpha maturity. The core workflow is tested and usable, but public interfaces may evolve before 1.0. It supports Python 3.12, 3.13, and 3.14 and is licensed under Apache-2.0.

Install

Install the CLI with uv:

uv tool install permissiondiff

Or install it in an active virtual environment with pip:

python -m pip install permissiondiff

To add PermissionDiff to an existing uv project instead, run uv add permissiondiff.

Five-minute quickstart

After installation:

permissiondiff init demo
cd demo
permissiondiff test

init creates a runnable config and a deliberately vulnerable authorizer. The test reports a minimized cross-tenant reproduction under .permissiondiff/failures/ and exits 1 because the tenant-isolation invariant fails.

Authorizer interface

Point PermissionDiff at one ordinary decision function:

from permissiondiff import Action, Context, Decision, Resource, Subject


def authorize(
    subject: Subject,
    action: Action,
    resource: Resource,
    context: Context,
) -> Decision:
    if subject.tenant != resource.tenant:
        return Decision.DENY
    if action.name == "read_invoice" and subject.role in {"support", "admin"}:
        return Decision.ALLOW
    return Decision.DENY

The result must be exactly Decision.ALLOW or Decision.DENY. Crashes, hangs, and invalid return values are explicit evaluation errors and exit 3; PermissionDiff never fails open.

Configuration

The YAML describes real entities rather than asking a fuzzer to invent arbitrary application objects:

authorizer: auth:authorize

subjects:
  - {id: alice, tenant: acme, role: support}
  - {id: bob, tenant: globex, role: support}

resources:
  - {id: invoice-acme, type: invoice, tenant: acme, owner_id: alice}
  - {id: invoice-globex, type: invoice, tenant: globex, owner_id: bob}

actions: [read_invoice, refund]
contexts:
  amounts: {min: 0, max: 1000, boundaries: [499, 500, 501]}

invariants:
  - tenant_isolation
  - role_boundary: {action: refund, allowed_roles: [admin]}
  - ownership: {actions: [read_invoice]}

fail_on:
  invariant_violation: true
  newly_allowed: true
  newly_denied: false

generation: {max_examples: 64}
execution: {timeout_seconds: 2}

Hypothesis combines declared entities and explores numeric boundaries. With the same PermissionDiff and Python versions, configuration, seed, exact corpus, and deterministic authorizer, findings are semantically reproducible. Canonical JSON, stable case fingerprints, stable finding ordering, and fixed IDs make review practical. A nondeterministic authorizer remains nondeterministic; PermissionDiff does not conceal that.

Test and invariant workflow

uv run permissiondiff test --config permissiondiff.yaml --seed 42
uv run permissiondiff explain PD-0001

Built-in invariants cover tenant isolation, action-specific role boundaries, and ownership. A custom deterministic invariant can be declared as:

invariants:
  - custom: {name: refund_limit, callable: invariants:refund_limit}

The callable receives (AuthorizationCase, Decision) and returns bool or InvariantResult.

Baseline and diff workflow

Create a baseline on trusted code:

uv run permissiondiff snapshot --config permissiondiff.yaml \
  --output .permissiondiff/main.json --seed 42

The snapshot stores every exact input case, its baseline decision, a stable fingerprint, the corpus seed, and schema/package versions. On candidate code, replay it:

uv run permissiondiff diff --config permissiondiff.yaml \
  --baseline .permissiondiff/main.json

Classification is exhaustive:

Baseline Candidate Result
DENY DENY unchanged_denied
ALLOW ALLOW unchanged_allowed
DENY ALLOW newly_allowed
ALLOW DENY newly_denied

A newly allowed path is security-sensitive, not automatically a vulnerability. fail_on decides what blocks CI. Invariants can still catch a vulnerability already present in the baseline.

The complete machine report is .permissiondiff/report.json by default.

CI

name: permissiondiff
on: [pull_request]
jobs:
  authorization:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v6
      - run: uv sync --all-groups
      - run: uv run permissiondiff diff --baseline .permissiondiff/main.json

Exit codes are stable: 0 pass, 1 configured policy/invariant failure, 2 configuration or snapshot error, 3 evaluation/runtime error.

The repository's thin composite action invokes the same CLI without duplicating its logic:

- uses: abishekgiri/permissiondiff@v0
  with:
    config: permissiondiff.yaml
    baseline: .permissiondiff/main.json

Security and limitations

Only point PermissionDiff at decision logic with no live side effects. Never use an authorizer that issues refunds, deletes data, sends messages, or contacts production.

Authorizers run in a timeout-controlled subprocess to isolate crashes and hangs from the main CLI. This subprocess is not a security sandbox. Imported Python code has the operating-system permissions of the user running PermissionDiff and may be malicious. Use trusted code and isolated CI environments.

Snapshots and reproductions may contain sensitive identifiers. Treat them accordingly. v0 supports local Python authorizers and explicit domains; it does not include Git worktree orchestration, remote policy engines, live agent/tool interception, a dashboard, or least-privilege mining.

Report vulnerabilities privately as described in the security policy. For bugs and feature requests, use GitHub Issues.

Development

uv sync --all-groups
uv run ruff check .
uv run ruff format --check .
uv run mypy src tests
uv run pytest
uv run pytest --cov=permissiondiff --cov-report=term-missing
uv build

The roadmap is deliberately short: prove authorization diffs are useful, then consider policy-engine adapters, Git-aware orchestration, agent principals/delegation, safe shadow interception, and least-privilege reduction with proof.

See the contribution guide for the contribution workflow and the changelog for release history.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

permissiondiff-0.1.0.tar.gz (19.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

permissiondiff-0.1.0-py3-none-any.whl (26.8 kB view details)

Uploaded Python 3

File details

Details for the file permissiondiff-0.1.0.tar.gz.

File metadata

  • Download URL: permissiondiff-0.1.0.tar.gz
  • Upload date:
  • Size: 19.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for permissiondiff-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ae66cff27cc020cd48ec0b395c6c4fbe59456da5618f2315bb89b8dc821cbbed
MD5 3fb2ce29740ea10b0f4ee844d2bf4d51
BLAKE2b-256 ffbfccd02552b5c8735fabcc71b8563d7c3a3f92b2e72da263cbc124cb6bd08c

See more details on using hashes here.

Provenance

The following attestation bundles were made for permissiondiff-0.1.0.tar.gz:

Publisher: release.yml on abishekgiri/permissiondiff

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file permissiondiff-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: permissiondiff-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 26.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for permissiondiff-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 57aff5e724989dacd6c9b5d55c1f57e7240b25d7bcb1a79ad0f8bed529b58d6b
MD5 89a4f2c2fada06adea695e8135e51f56
BLAKE2b-256 8cd13c5865ee59bc91d1a0fb93192113087ad1ee795e3ae5b4dd93f38daa8f2a

See more details on using hashes here.

Provenance

The following attestation bundles were made for permissiondiff-0.1.0-py3-none-any.whl:

Publisher: release.yml on abishekgiri/permissiondiff

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.0

2 files

0.1.1

2 files

This release

0.1.0 This release

2 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