Skip to main content

django-opa-permissions

Object-level Django permissions backed by OPA (Rego) policies stored in the database, evaluated in-process via opa-golib-python-bindings — no OPA server needed.

Features

  • Policies as DB objects — Policy (UUID pk, Rego source) grouped into PolicySets via an ordered membership (a policy may belong to several sets); a PolicySetBinding maps a Django model (ContentType) to the policyset governing it. Unbound models deny everything (superusers excepted).
  • Auth backend — OpaPermissionBackend implements user.has_perm("app.view_book", obj) so Django and DRF (DjangoObjectPermissions) work out of the box. Designed for subclassing: many small hook methods (extend_input, get_builtins, is_bypass, filter_result, …).
  • Minimal input document — policies receive only the user pk, app label, model name, action, and (except for browse/create) the object pk. Data is fetched from inside the policy via custom builtins (django_opa_fetch, django_opa_fetch_old, django_opa_query) — ORM-backed, no SQL.
  • Browse partial evaluation — Model.objects.for_user(user) compiles the policy's filter rule with the object left unknown (OPA partial evaluation → UCAST → Django Q/Exists), filtering listings in SQL; relation traversal and $some/$all/$none quantifiers are supported. The authoritative full check (with the object pk) runs in allowed_for_user / has_perm.
  • Admin integration — OpaModelAdminMixin wires a ModelAdmin to OPA: the changelist is gated by the browse permission and filtered with the browse prefilter; object pages check view/change/delete per object. Classic Django model permissions remain a fallback (see Security notes).
  • Admin policy debugger — evaluate a policyset as any (visible) user against any bound model/object: print() output inline next to the policy line, per-line coverage highlighting, the full output document as a tree, and the residual browse prefilter. No trace logs. Requires the change permission on the policy (see Security notes).
  • Admin permission explorer — linked from the Policy changelist: answers "who is allowed to library.change_book (on object 7)?" as a table over all visible users, and "what may alice do?" as a table of every permission string of each bound model (Django's default and Meta.permissions codenames, plus the browse pseudo-permission; browse rows also report how many objects the prefilter yields). Only models whose governing policies you may all change are offered.

Install

pip install django-opa-permissions
INSTALLED_APPS = [..., "django_opa_permissions"]
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "django_opa_permissions.backends.OpaPermissionBackend",
]

Quick start

from django_opa_permissions.managers import OpaPermissionedModel

class Book(OpaPermissionedModel):
    title = models.CharField(max_length=300)
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    published = models.BooleanField(default=False)

Create a PolicySet, add a Policy, and bind it to the Book content type (all in the admin). Every policy must declare package policies; the entry points are allow (full evaluation) and filter (browse partial evaluation):

package policies

import rego.v1

# Owners may do anything with their own books.
allow if {
    obj := django_opa_fetch(input.app_label, input.model, input.object.id)
    obj.owner_id == input.user.id
}

# Anyone may view or browse published books.
allow if {
    input.action in {"view", "browse"}
    obj := django_opa_fetch(input.app_label, input.model, input.object.id)
    obj.published == true
}

# Browse prefilter: the object is UNKNOWN here — reference its columns via
# input.object; this compiles to SQL through the ORM.
filter if {
    input.object.published == true
}

filter if {
    input.object.owner_id == to_number(input.user.id)
}
Book.objects.for_user(user)          # SQL prefilter (partial evaluation)
Book.objects.allowed_for_user(user)  # prefilter + authoritative per-object pass
user.has_perm("library.view_book", book)
book.user_can(user, "change")        # or "delete", custom actions, ...
modified.user_can(user, "change", old=snapshot)  # policies may fetch both states

See docs/ for the input schema, builtins, filter-rule grammar (relation paths and quantifiers), and subclassing the backend; example_project/ is a runnable sqlite demo:

cd example_project
python manage.py migrate
python manage.py load_example_policies
python manage.py create_demo_users   # alice/alice-password, bob/bob-password,
                                     # admin/admin-password (superuser)
python manage.py runserver

Security notes

Read this before rolling out in a hostile environment. The design trades a few guarantees for admin ergonomics; every point below is deliberate and configurable.

  • Admin falls back to classic Django permissions. With OpaModelAdminMixin, every OPA decision is OR-ed with the classic ModelAdmin check: a user holding a traditional Django model permission (e.g. view_book via groups + ModelBackend) gets full, unfiltered access to that model in the admin — OPA row-level denies do not apply to them. This keeps stock Django workflows working, but it means your Django groups must not hold permissions on OPA-governed models if you want OPA to be the sole authority. Audit django.contrib.auth.models.Permission assignments on bound models accordingly.
  • Policy authors can read your whole database. The builtins (django_opa_fetch, django_opa_fetch_old, django_opa_query) serialize every concrete column of any model — including sensitive ones such as User.password (the hash), tokens or secrets stored on rows — and they use _base_manager, bypassing any permission-filtering manager. Anyone who can create or edit a Policy (and anyone able to open the policy debugger or permission explorer) can therefore read data your application policies are meant to protect. Treat policy authorship as a highly privileged role; if you need redaction, override serialize_instance / get_builtins in a backend subclass to strip sensitive fields.
  • Superusers bypass OPA by default. DJANGO_OPA_SUPERUSER_BYPASS (default True) lets superusers skip all policy checks, including the default-deny for unbound models. Set it to False in settings if superusers must be policy-governed too.
  • The full OPA builtin surface is available to policies. Beyond the Django builtins, policies run on the embedded OPA engine (opa-golib-python-bindings), so whatever builtins that engine exposes — potentially including http.send, net.*, opa.runtime and crypto.* — is callable from a policy. A policy author can make network requests or read runtime configuration from inside your request path. Verify the engine's capabilities for your deployment (e.g. via a capabilities file) and restrict who may author policies.

License

MIT

Metadata

Release files for django-opa-permissions 0.5.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 django-opa-permissions 0.5.0
File Size Uploaded
django_opa_permissions-0.5.0.tar.gz 51.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-opa-permissions 0.5.0
File Interpreter ABI Platform
django_opa_permissions-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 88.8 kB

Release files / django_opa_permissions-0.5.0.tar.gz

Download URL django_opa_permissions-0.5.0.tar.gz
Size 51.7 kB
Tags Source
SHA-256 checksum
How to use checksums
47fc243d51a8c68b6ddf28cc849c2bff2a5d18c2f171451769c5e37ee5c31dd5
BLAKE2b-256 checksum
How to use checksums
437d11c7a77e465aa27a749b0b932318c392ace1e8142510ffeb5db9b3b516eb
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 Sep 19, 2026.

Transparency log

Release files / django_opa_permissions-0.5.0-py3-none-any.whl

Download URL django_opa_permissions-0.5.0-py3-none-any.whl
Size 37.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
afee3754505eaae54c4d7c7c83a1418d8fc71ad52b09b535a99600ae870d2227
BLAKE2b-256 checksum
How to use checksums
884a4cde58dec0b9c885bdb449db7038728c53d641159155daccc51cbfc7d610
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 Sep 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

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