In Django, if a user holds 'listings.change_listing', may they edit every listing, and what does has_perm() return with obj=listing?
answer
- permissions are per model
- the obj argument is a hook
- default backend returns empty
- superusers still pass
basics
~10 sYes: model permissions are global, so change_listing covers every listing. has_perm('listings.change_listing', listing) returns False with only ModelBackend, because it grants nothing when an object is passed; only an active superuser passes.
solid answer
~40 sA Django model permission applies to the whole model, so `listings.change_listing` means "may change listings" with no notion of ownership, and any check made without an object passes for every listing. The API accepts an `obj` argument — `user.has_perm("listings.change_listing", listing)` — and passes it to every backend, but the default `ModelBackend` returns an empty set whenever `obj` is given, so the answer is `False`. It does not fall back to the model-level grant and never inspects the object. Only an active superuser passes, through the shortcut that skips backends. Per-object rules come from scoping queries to the user, a custom backend, or an object-permission package.
go deeper
Remember that a model permission covers every row, and that adding obj to has_perm() returns False unless something extra answers it.
Explain how the obj argument travels through PermissionsMixin to each backend and why ModelBackend returns an empty set for it.
Choose between queryset scoping, a custom backend and a package, and make sure every caller actually passes the object.
Decide where row-level authorization lives in the codebase so that every entry point — views, APIs, admin, jobs — applies the same rule.
## Model-level permissions are global A Django **model permission** such as `listings.change_listing` is a row in `auth_permission` tied to a *model*, not to a row of that model. The docstring on `Permission` states it plainly: permissions are set globally per type of object, so "Mary may change listings" is expressible, while "Mary may change listings, but only the ones she created" is not. So in a property-listings app, granting an owner `change_listing` so they can edit their flat also lets them pass every check that asks only "may this user change listings?" — which is every check written without an object. ## The `obj` argument exists, but core answers "no" The permission API was designed with objects in mind. `has_perm()`, `has_perms()`, `get_all_permissions()` and friends all accept an optional `obj`: ```python request.user.has_perm("listings.change_listing", listing) ``` `PermissionsMixin` passes the object on to every backend in `AUTHENTICATION_BACKENDS`. The default `ModelBackend`, however, returns an **empty set** whenever `obj is not None`, so its answer is `False`. The Django documentation calls this a foundation for object permissions with **no implementation in core**: object checks always return `False` or an empty collection until you add something that answers them. | Call (non-superuser holding `change_listing`) | Result with only `ModelBackend` | |---|---| | `user.has_perm("listings.change_listing")` | `True` | | `user.has_perm("listings.change_listing", listing)` | `False` | | `user.get_all_permissions(listing)` | `set()` | | Same object call for an active superuser | `True` — the superuser shortcut runs before any backend | Two things to notice: - `ModelBackend` does **not** fall back to the model-level answer when an object is given. The object form is a separate question, and core has no answer to it. - Nothing inspects the object. Passing `listing` does not make Django compare `listing.owner` with the user; there is no convention it could follow. ## What people expect instead A common misunderstanding is that `obj` is a filter on top of the model permission — "has the right *and* it applies to this listing". In reality the two calls are independent questions. If you switch a view from `has_perm(perm)` to `has_perm(perm, listing)` without adding a backend, every non-superuser is suddenly refused. ## How object-level rules are actually added There are three common routes, from simplest to most general: 1. **Scope the lookup to the user** — fetch the listing from `Listing.objects.filter(owner=request.user)`, so a listing that is not yours is simply not found. 2. **Write a custom authorization backend** whose `has_perm(user_obj, perm, obj)` returns `True` when `obj.owner_id == user_obj.pk`, and add it to `AUTHENTICATION_BACKENDS` next to `ModelBackend`. 3. **Use an object-permission package** that stores per-object grants or evaluates rule predicates, and plugs in as another backend. ## Callers must pass the object Even with a backend in place, an object rule only runs when the caller supplies the object. Django's own `permission_required` decorator and `PermissionRequiredMixin` check `has_perms()` **without** an object by default, and the admin's default `has_change_permission()` ignores the `obj` it receives. Object-level checks therefore have to be called explicitly, with the instance in hand, wherever the decision is made. ## Anonymous users reach backends too The object hook is not limited to logged-in users. `AnonymousUser.has_perm()` also walks the configured backends, passing the anonymous user object. `ModelBackend` returns an empty set for anonymous users, but a custom backend may answer — for example, allowing anyone to view listings whose `is_public` flag is set: ```python def has_perm(self, user_obj, perm, obj=None): if perm == "listings.view_listing" and obj is not None: return obj.is_public return False ``` Django's docs point out that this lets anonymous visitors hold permissions that inactive authenticated users do not, because inactive users are refused by `ModelBackend` while the anonymous object is handed to your rule unchanged. Whether that is desirable depends on the rule; it is one more reason custom backends should check `is_active` and `is_anonymous` explicitly. ## Summary for an interview - Model permissions are global per model. - `has_perm(perm, obj)` reaches every backend, but `ModelBackend` answers `False` for any object. - Active superusers still pass, because their shortcut ignores backends. - Row-level rules need scoping, a custom backend or a package — Django core provides only the hook.
- Why does an active superuser pass has_perm(perm, listing) when ModelBackend refuses objects?`PermissionsMixin.has_perm()` returns `True` for an active superuser before any backend is consulted, with or without an object. The backend's refusal never runs, so superusers pass every object check — which also means testing object rules as a superuser proves nothing.
- Does Django's permission_required decorator pass the object to has_perm()?No. The decorator and `PermissionRequiredMixin` call `has_perms()` with the permission names only, so an object-level backend is never consulted through them. You call `request.user.has_perm(perm, obj)` yourself once the object is loaded, or customise the check to supply it.
A model permission is like a master key for every flat in a building. Asking the default key cabinet about one specific flat gets a blank stare: it only knows about master keys, and someone has to add a cabinet that knows which flat belongs to whom.
saying these in an interview costs you the question
- Passing obj makes Django compare the object's owner with the user
- ModelBackend falls back to the model permission when obj is given
- change_listing lets a user change only the listings they created
- Object permissions work out of the box once contrib.auth is installed
- A superuser fails object checks because ModelBackend returns an empty set