In Django's contrib.auth, which permissions does every model get automatically, and how do you check one with has_perm()?
answer
- four actions per model
- rows written after migrate
- post_migrate handler
- app label, dot, codename
basics
~10 sDjango creates add, change, delete and view permissions for every model when migrate runs. You check one with user.has_perm('app_label.codename'), for example has_perm('newsroom.change_article'), where the codename is the action plus the lowercase model name.
solid answer
~40 sWith `django.contrib.auth` installed, every model gets four `Permission` rows — `add_<model>`, `change_<model>`, `delete_<model>` and `view_<model>` — created by a `post_migrate` handler each time `migrate` runs, not by `makemigrations`. You check them with a string, `user.has_perm("newsroom.change_article")`: the app label, a dot, and the codename. `has_perm()` returns `True` at once for an active superuser; otherwise the `ModelBackend` checks whether that string is in the set of permissions the user holds directly or through groups. `has_perms()` takes a list and requires all of them. These permissions are global per model and enforce nothing by themselves — the admin uses them, and your own views only respect them if they check.
code
python · 11 linesfrom django.contrib.auth import get_user_model
User = get_user_model()
editor = User.objects.get(username="dana")
editor.has_perm("newsroom.view_article")
editor.has_perm("newsroom.change_article")
editor.has_perms(["newsroom.view_article", "newsroom.change_article"])
# A bare string is rejected:
# editor.has_perms("newsroom.view_article") -> ValueErrorgo deeper
Recall the four actions — add, change, delete, view — and the exact check string: app label, dot, action underscore lowercase model name.
Explain that a post_migrate handler creates the rows, that has_perm() short-circuits for active superusers and otherwise asks the backends, and that has_perms() needs an iterable.
Show you know the rows enforce nothing on their own, that migrate never deletes obsolete ones, and how the 6.1 rename handling changes a model-rename rollout.
Weigh leaning on the built-in per-model rows against a custom permission scheme, given that they are global per model and carry no object scope.
## The four default permissions When `django.contrib.auth` is in `INSTALLED_APPS`, Django gives **every model in every installed app** four **model-level permissions**: **add**, **change**, **delete** and **view**. They are ordinary rows in the `auth_permission` table, modelled by `django.contrib.auth.models.Permission`. Each row has three meaningful columns: - `content_type` — a foreign key to `ContentType`, which identifies the model (app label plus model name); - `codename` — a machine name such as `change_article`, built as the action, an underscore and the model's lowercase `model_name`; - `name` — a human label such as "Can change article", shown in the admin's permission picker. A permission in this sense is **global per model**: "Dana may change articles" — not "Dana may change *this* article". Per-object rules are a separate mechanism. For a newsroom app labelled `newsroom` with an `Article` model, the rows look like this: | Action | Codename | String passed to `has_perm()` | |---|---|---| | add | `add_article` | `"newsroom.add_article"` | | change | `change_article` | `"newsroom.change_article"` | | delete | `delete_article` | `"newsroom.delete_article"` | | view | `view_article` | `"newsroom.view_article"` | ## When the rows are created The rows are not written by `makemigrations`, and they are not created lazily on first use. `django.contrib.auth` connects a handler, `create_permissions`, to the **`post_migrate` signal**. Every time `manage.py migrate` finishes, that handler walks each installed app's models, computes the permissions each one should have and **bulk-creates the missing ones**. Two consequences follow: 1. A brand-new model has no permission rows until `migrate` has run to completion — so code that runs *inside* a migration for that model will not find them yet. 2. The handler only **adds**. It does not delete rows for permissions you later stop declaring. Since **Django 6.1**, a second `post_migrate` handler also renames the four default permissions (codename and name) when a `RenameModel` operation renames the model, so `change_story` becomes `change_article` instead of being left behind next to a freshly created row. ## The permission string: `app_label.codename` Checks never use the `Permission` object directly. They use a **string** of the form `"<app_label>.<codename>"`: - the **app label** is the `AppConfig.label` — by default the last component of the app's module path, so `apps.newsroom` is labelled `newsroom`; - the **codename** is the lowercase codename stored on the row. The model name appears only because it is baked into the default codenames. Django 6.1 adds `Permission.user_perm_str`, a property that returns exactly this string from a `Permission` row. ## What `has_perm()` actually does `user.has_perm(perm, obj=None)` is defined on `PermissionsMixin`, which the default `User` and most custom user models include. Its logic: 1. If the user is **active and a superuser**, return `True` immediately — no lookup at all. 2. Otherwise ask each configured authentication backend in turn; the first backend that answers `True` wins. 3. The default `ModelBackend` returns `False` for inactive or anonymous users, and otherwise loads the set of `"app_label.codename"` strings the user holds (direct grants plus group grants) and tests **membership** in that set. `has_perms(["newsroom.view_article", "newsroom.change_article"])` returns `True` only if every permission in the list passes. It requires an iterable of strings; passing a single bare string raises `ValueError`. ## Related methods worth naming The same `PermissionsMixin` offers a few siblings of `has_perm()` that interviewers like to hear named: - **`ahas_perm()` and `ahas_perms()`** — asynchronous variants for async views, with the same superuser shortcut and the same string format; - **`has_module_perms(app_label)`** — `True` if the user holds *any* permission whose string starts with that app label; the admin index uses it to decide which apps to list; - **`get_all_permissions()`** — returns the full set of `"app_label.codename"` strings, which is handy when debugging why a check fails. All of them read the same set that `ModelBackend` builds, so they agree with `has_perm()` for a given user object. ## What default permissions do not do They are **data, not enforcement**. The admin consults them to decide who may add, change, delete or view objects, but your own views, forms and APIs are unaffected until code calls `has_perm()` (directly or through a decorator or mixin). A model with no checks anywhere is editable by any code path that reaches it, whatever the permission table says.
- What does has_perm() return for an active superuser?Always `True`. `PermissionsMixin.has_perm()` returns `True` for an active superuser before any backend is consulted, even for a permission string that does not exist. An inactive superuser gets no such shortcut, and `ModelBackend` returns `False` for inactive users, so deactivating a superuser does remove their rights.
- If you add a new model and immediately write a data migration for it in the same migrate run, will its default permissions exist inside that migration?No. The rows are created by the `post_migrate` handler, which runs after all migrations in the run have finished. Code inside a migration that expects `add_<model>` for a model created earlier in the same run will not find it; the usual fix is to create the needed permission rows explicitly or to do the work after migrate.
saying these in an interview costs you the question
- makemigrations writes the permission rows into the migration file
- The codename keeps the class name's capitals, like change_Article
- The app label is the full module path, like apps.newsroom
- Default permissions automatically block every view that edits the model
- has_perm() takes the model class and an action name