skip to content

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%

answer

  1. the first check remembers
  2. stored on the instance
  3. refresh_from_db is not enough
  4. load a new user object

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.

solid answer

~40 s

`ModelBackend` stores the permission strings on the user object the first time a check needs them — in private attributes such as `_perm_cache` — and later checks on that same instance read the cached set. `user_permissions.add()` or `groups.add()` writes to the database but leaves the cached set alone, so the second `has_perm()` answers from stale data. The documented fix is to load a new instance with `User.objects.get(pk=user.pk)`; `refresh_from_db()` reloads fields only and does not clear the cache. The cache lives only as long as that object, so the next request sees the change — the trap shows up in tests, in grant-then-check views and in long-running processes, and it applies to revocations too.

code

python · 22 lines
python
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.test import TestCase


class PublishGrantTests(TestCase):
    def test_grant_visible_only_on_a_new_instance(self):
        User = get_user_model()
        editor = User.objects.create_user("dana", password="pw-for-tests")
        self.assertFalse(editor.has_perm("newsroom.publish_article"))

        perm = Permission.objects.get(
            codename="publish_article", content_type__app_label="newsroom"
        )
        editor.user_permissions.add(perm)
        self.assertFalse(editor.has_perm("newsroom.publish_article"))

        editor.refresh_from_db()
        self.assertFalse(editor.has_perm("newsroom.publish_article"))

        editor = User.objects.get(pk=editor.pk)
        self.assertTrue(editor.has_perm("newsroom.publish_article"))

go deeper

for a junior

Remember that a user object remembers its permissions after the first check, so re-load the user from the database after granting.

for a middle

Explain which attributes ModelBackend fills, why m2m adds do not clear them, and why refresh_from_db() is insufficient.

for a senior

Reason about where staleness actually bites — same-request flows, tests, long-running processes, and revocation — and choose re-fetching over poking private attributes.

for a principal

Decide whether long-running components need an explicit re-fetch policy before sensitive checks, weighing query cost against how quickly revocations must apply.

## The symptom A newsroom has a "promote to editor" view. It grants the custom permission `newsroom.publish_article` and then, in the same function, checks it to decide what to show next: ```python from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission User = get_user_model() def promote(user_id): user = User.objects.get(pk=user_id) user.has_perm("newsroom.publish_article") # False; fills the cache perm = Permission.objects.get( codename="publish_article", content_type__app_label="newsroom" ) user.user_permissions.add(perm) return user.has_perm("newsroom.publish_article") # still False ``` The grant is in the database, yet the second check returns `False`. ## The cause: a per-instance cache in `ModelBackend` `django.contrib.auth.backends.ModelBackend` avoids re-querying on every check by **caching the permission set on the user object itself**. On the first check it runs the queries and stores Python sets of `"app_label.codename"` strings in private attributes: - `_user_perm_cache` — permissions granted directly through `user_permissions`; - `_group_perm_cache` — permissions inherited through `groups`; - `_perm_cache` — the union of both, used by `has_perm()`. Later checks on **the same instance** read the attributes and never query again. Nothing invalidates them: `user_permissions.add()`, `groups.add()` or a removal writes to the join tables but does not touch the cached sets, and Django sends no signal that clears them. The Django documentation adds a detail that catches most people: **`user.refresh_from_db()` does not clear the cache**. It reloads field values; the cache attributes are not fields, so they survive. ## Fixes 1. **Re-fetch the user.** `user = User.objects.get(pk=user.pk)` gives a new instance with no cache attributes; the next check queries fresh. This is the documented approach. 2. **Check before caching.** Reorder the code so no check runs on that instance until after the grant. 3. **Delete the private attributes.** Removing `_perm_cache`, `_user_perm_cache` and `_group_perm_cache` works today, but they are undocumented internals — prefer re-fetching. ## How far the staleness reaches The cache lives exactly as long as the Python object. That bounds the problem: | Situation | Stale? | |---|---| | Next HTTP request for the same user | No — the request's user is loaded as a new instance | | Later in the same request, same `request.user` object | Yes | | A test that grants then asserts on the same instance | Yes | | A long-running process holding one user object (a worker loop, a management command, a long-lived connection handler) | Yes, until re-fetched | It is **not** a cross-request or cross-process cache: it never touches the `CACHES` backends or the session. The same staleness applies to **revocation** — removing a grant does not affect an instance that already cached the old set, which matters in long-running code that should stop an editor mid-session. ## Two cases that never hit the cache - **Active superusers**: `PermissionsMixin.has_perm()` returns `True` before any backend runs, so granting or revoking has no visible effect either way. - **Inactive or anonymous users**: `ModelBackend` returns an empty set without caching anything. ## Choosing a fix in practice - **In a view that grants and then renders**, re-fetch the user after the grant and pass the new instance to the template or to later checks; do not keep using the stale one. - **When the granted user is `request.user` itself**, remember that `request.user` is the stale object for the rest of the request; reassigning it to a newly fetched instance is acceptable, but often the simpler answer is to redirect, so the next request loads the user afresh. - **In long-running code**, re-fetch the user (or at least re-check against a newly loaded instance) before any decision that must reflect a recent revocation, such as allowing a publish. - **In tests**, load a new instance by primary key after changing grants; avoid asserting on the object you used before the grant. Each of these costs one extra query at most, which is almost always cheaper than debugging a check that lies. ## Why tests are where this shows up Tests are the classic place to meet the cache, because a test naturally creates a user, checks, grants, and checks again on one object. A test that seems to prove "the grant does not work" is often proving the cache works. Re-fetch the user between the grant and the assertion, or assert on a new instance loaded by primary key.

  • Does the cache mean a revoked editor keeps publishing rights until they log out?
    No. The cache is attached to one user instance, and each request loads a new instance, so the revocation applies from the next request. It stays stale only within the current request or in a long-running process that keeps the same user object, which should re-fetch before sensitive checks.
  • Would the same grant-then-check code behave differently for a superuser?
    Yes: it would return `True` both times. `PermissionsMixin.has_perm()` returns `True` for an active superuser before any backend or cache is consulted, which is also why tests run as a superuser can hide a missing grant.

A doorman is handed a printed guest list at the start of his shift. Adding a name at the office does not change the copy in his hand; he only admits the new guest once someone gives him a freshly printed list, which is what re-fetching the user does.

saying these in an interview costs you the question

  • refresh_from_db() reloads the user's permissions as well
  • The permission cache lives in the CACHES backend across processes
  • A revoked user keeps the right until the session expires
  • user_permissions.add() automatically clears the permission cache