skip to content

Per-Model Codenames

Every model gets add, change, delete and view permissions checked as 'app_label.codename' strings through has_perm(). Interviewers probe custom Meta.permissions and the stale permission cache.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Django's contrib.auth, which permissions does every model get automatically, and how do you check one with has_perm()?

level: juniorimportance: must knowfreq 62%

answer

  1. four actions per model
  2. rows written after migrate
  3. post_migrate handler
  4. app label, dot, codename

basics

~10 s

Django 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 s

With `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 lines
python
from 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")  -> ValueError

go deeper

for a junior

Recall the four actions — add, change, delete, view — and the exact check string: app label, dot, action underscore lowercase model name.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

In a Django newsroom app, how do you add a custom 'can publish' permission to an Article model, and what does declaring it enforce?

level: middleimportance: should knowfreq 48%

basics

~10 s

Declare Meta.permissions = [('publish_article', 'Can publish article')] on Article and run migrate, which creates the Permission row. The declaration enforces nothing: views must still check has_perm('newsroom.publish_article') before publishing.

open as a page

In Django, which permission-string mistakes make user.has_perm() silently return False, and how does Permission.user_perm_str in 6.1 help?

level: middleimportance: should knowfreq 30%

basics

~20 s

has_perm() only tests whether an 'app_label.codename' string is in the user's set, so a missing app label, a module path, a capitalised model name or a typo just returns False. Django 6.1's Permission.user_perm_str builds the correct string from a Permission row.

open as a page

In Django, a view grants an editor 'newsroom.publish_article' and then calls has_perm() on the same user object, which returns False — why, and how do you fix it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

ModelBackend caches a user's permission set on the user instance at the first check, and adding a grant does not invalidate it. Re-fetch the user with User.objects.get(pk=...) before checking again; refresh_from_db() does not clear the cache.

open as a page