How would you write a Django authorization backend so has_perm('listings.change_listing', listing) is True only for that listing's owner?
answer
- a backend that authenticates nobody
- first True wins
- False means no opinion
- raise to veto
- callers must pass obj
basics
~20 sSubclass BaseBackend, override has_perm(user_obj, perm, obj) to return True when obj is a Listing whose owner_id equals the active user's pk, and add it to AUTHENTICATION_BACKENDS after ModelBackend. Callers must pass the listing to has_perm().
solid answer
~40 sA class based on `BaseBackend` overriding only `has_perm(user_obj, perm, obj=None)` authenticates nobody but adds permissions. It returns `False` unless `obj` is a `Listing`, the permission is one it owns, the user is active and `obj.owner_id == user_obj.pk`; you register it in `AUTHENTICATION_BACKENDS` alongside `ModelBackend`. Returning `False` means "no opinion", so other backends still answer; the first `True` wins, and raising `PermissionDenied` vetoes and stops the loop — though never for active superusers, whose shortcut runs first. The rule only fires when callers pass the object: `permission_required`, `PermissionRequiredMixin` and the admin's default permission methods check without it. For list pages, filter the queryset instead of checking rows one by one.
go deeper
Know that extra permission rules come from classes listed in AUTHENTICATION_BACKENDS, and that has_perm() receives the object.
Explain the loop over backends: superuser shortcut, first True wins, PermissionDenied stops it, and BaseBackend's harmless defaults.
Write a cheap, is_active-aware rule, order allow and veto backends deliberately, and make sure views and the admin pass the object.
Judge when a rule backend stops scaling and grants need to become data, and who owns that authorization layer.
## What an authorization backend is Every entry in `AUTHENTICATION_BACKENDS` can answer two kinds of questions: *who is this?* (`authenticate()`, `get_user()`) and *may they do this?* (`has_perm()`, `get_all_permissions()`, `has_module_perms()`, `with_perm()`). A backend can do only the second. `django.contrib.auth.backends.BaseBackend` supplies harmless defaults — `authenticate()` returns `None`, permission methods return empty sets — so a subclass that overrides only `has_perm()` authenticates nobody and adds permissions. The user model delegates to the backends in order. The documented rules: 1. An **active superuser** gets `True` before any backend is asked. 2. Otherwise each backend is asked in turn; the **first `True` wins** — the permissions a user has are the superset of what all backends grant. 3. A backend that **raises `PermissionDenied`** stops the loop, and the answer is `False` — the remaining backends are not asked. ## The backend For a listings app where owners may change and delete their own listings: ```python from django.contrib.auth.backends import BaseBackend from listings.models import Listing OWNER_PERMS = {"listings.change_listing", "listings.delete_listing"} class ListingOwnerBackend(BaseBackend): def has_perm(self, user_obj, perm, obj=None): if obj is None or perm not in OWNER_PERMS: return False if not user_obj.is_active or user_obj.is_anonymous: return False return isinstance(obj, Listing) and obj.owner_id == user_obj.pk ``` ```python # settings.py AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "listings.backends.ListingOwnerBackend", ] ``` Design choices worth defending in an interview: - **Return `False` for anything it does not own** — model-level calls (`obj is None`), other permissions, other models. `False` means "no opinion"; `ModelBackend` and other backends still get their turn. - **Check `is_active` yourself.** The docs warn that custom backends must; `PermissionsMixin` only applies the superuser shortcut and does not stop your backend from answering `True` for a deactivated owner. - **Compare `owner_id`, not `owner`.** The foreign-key id is already on the row; `obj.owner` would load the user from the database on every check. - **Keep it cheap.** It runs on every `has_perm()` call in the process, including checks about unrelated models. ## Allow versus veto Returning `True` *adds* a right. To *remove* one — "nobody may edit a listing while it is under review" — raise `PermissionDenied`: ```python from django.core.exceptions import PermissionDenied if obj is not None and getattr(obj, "under_review", False): raise PermissionDenied ``` Put a vetoing backend **first** in `AUTHENTICATION_BACKENDS`, because the loop stops at the first `True`. The veto cannot block active superusers: their shortcut happens before any backend runs. Keep allow rules and veto rules in separate, small backends rather than one class with many branches. Each can then be tested alone, reordered deliberately and removed without touching the other, and the settings list reads as the policy: first the vetoes, then `ModelBackend`, then the object rules that grant. ## Callers must pass the object The backend only fires when the object is supplied: - in a view: `request.user.has_perm("listings.change_listing", listing)` after loading the listing; - the `permission_required` decorator and `PermissionRequiredMixin` check without an object by default, so they never reach this rule; - the admin's default `has_change_permission()` ignores its `obj` argument; override it to call `request.user.has_perm(..., obj)` if the admin should apply the rule. ## Testing the backend Test the backend directly and through the user API, with realistic users rather than superusers: ```python from django.test import TestCase, override_settings BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "listings.backends.ListingOwnerBackend", ] @override_settings(AUTHENTICATION_BACKENDS=BACKENDS) class ListingOwnerBackendTests(TestCase): def test_owner_may_change_only_own_listing(self): ... ``` Cover the owner, a different owner, an inactive owner, an anonymous user and the model-level call without an object, which must be left to `ModelBackend`. ## Limits and alternatives | Concern | Consequence | |---|---| | List pages | A backend answers about one object; checking every row is N calls — filter the queryset by owner instead | | Async views | `user.ahas_perm()` calls each backend's `ahas_perm()`, and `BaseBackend`'s default derives it from `aget_all_permissions()`, **not** from your `has_perm()` — override `ahas_perm()` too, or the rule is silently absent in async code | | Per-object grants stored as data ("share this listing with my agent") | Needs a table of grants; object-permission packages provide one | | Many rule types | A rules-style package expresses them as composable predicates rather than one growing method |
- When would you reach for an object-permission package instead of this backend?When grants are data rather than a rule — an owner sharing one listing with a specific agent — you need a table of per-object grants and an admin for it, which packages such as django-guardian provide. When there are many rule-shaped checks, a predicate-based package such as django-rules keeps them composable. A single ownership rule is simpler as a small custom backend.
- An async view calls await request.user.ahas_perm('listings.change_listing', listing) and gets False for the owner. Why?The async path calls each backend's `ahas_perm()`. `BaseBackend`'s default implementation tests membership in `aget_all_permissions()`, which returns empty sets unless overridden; it never calls your synchronous `has_perm()`. Override `ahas_perm()` as well, for example by wrapping the sync rule with `sync_to_async`.
- Why order a vetoing backend first in AUTHENTICATION_BACKENDS?Django stops at the first backend that returns `True`. If `ModelBackend` or an allow-rule backend runs first and grants the permission, a later backend never gets the chance to raise `PermissionDenied`. Placing the veto first lets it refuse before anything grants.
saying these in an interview costs you the question
- A permissions-only backend must also implement authenticate() to work
- Returning False from a backend blocks all the backends after it
- PermissionDenied from a backend also blocks active superusers
- PermissionRequiredMixin passes the view's object to has_perm() automatically
- Comparing obj.owner costs nothing because foreign keys are always loaded