Filter concepts by levelShowing all levels.

Django · Section 38

Authorization

Level
intermediate
Read
20 min
Concepts
2

Authentication asks "who are you," authorization asks "what may you do" — the roadmap's own explicit distinction. Every model auto-gets add/change/delete/view permissions; Meta.permissions declares custom, project-specific ones (e.g. can_publish_article). A Group is a reusable, named bundle of permissions — the role-based-access-control pattern in miniature, since has_perm() checks both direct grants and every group a user belongs to. All of this is model-level, though: has_perm(perm, obj) accepts an object argument, but the default backend ignores it, so real object-level authorization ("can this user edit THIS Order") in most Django projects means explicit code — an equality check or a queryset filter — not a call into the permission system, unless a backend like django-guardian is added. DRF mirrors the same two-tier shape with has_permission() (view-level) and has_object_permission() (object-level, never called for list actions — get_queryset() filtering covers that instead). Admin-site permissions are a separate access surface from an application's own authorization rules, not a substitute for them.

This section

What is true here

  1. Authentication → who are you; Authorization → what may you do — two genuinely separate questions.
  2. Every model auto-gets add/change/delete/view permissions; Meta.permissions adds custom, project-specific ones.
  3. A Group is a reusable, named bundle of Permissions — has_perm() checks direct grants and every group a user belongs to.
  4. has_perm(perm, obj) exists but the default backend ignores obj — real object-level authorization is usually explicit code, not a permission-system call.
  5. DRF splits the same idea: has_permission() (view-level) vs has_object_permission() (never called for list actions — filter get_queryset() instead).

What you will be able to do

  • Add and grant custom, business-specific permissions via Groups rather than per-user
  • Implement real object-level and resource-level authorization, not just model-level checks
  • Use DRF's has_permission()/has_object_permission() split correctly, including for list vs detail actions
  • Keep admin-site permissions and application-level authorization as the separate layers they are

Model permissions, groups, and RBAC

The auto-created permissions, custom ones, and Groups as reusable roles.

Model permissions, groups, custom permissions, and RBAC

coreintermediate

Every model automatically gets four permissions (add/change/delete/view — codenames like "articles.add_article"), created by Django itself when migrations run. Custom permissions add project-specific ones (e.g. "can_publish_article") declared in a model's Meta.permissions. A Group bundles permissions for reuse: assign "Editor" the publish permission once, add users to "Editor," rather than granting it per-user. Role-based access control (RBAC) is the pattern this whole system implements — Groups act as roles, permissions are what a role can do, and a user's effective permissions are the union of their own direct permissions plus every group they belong to.

Think of it as

Django auto-creates add/change/delete/view for EVERY model specifically so the common case ("can this user edit Articles at all") needs zero custom code — just checking user.has_perm("articles.change_article"). Custom permissions exist because real authorization almost always needs verbs beyond CRUD (publish, approve, refund, export) that have no generic equivalent — Meta.permissions is how a model declares "this business action needs its own permission," created by the same migration machinery as the automatic four. Groups exist purely because "grant these 6 permissions to these 40 users" doesn't scale as 240 individual grants — a Group is a single reusable record that permissions attach to once, and users attach to the group instead of the permissions directly, which is the entire RBAC pattern in miniature: roles (groups) sit between users and permissions so that changing what a "Moderator" can do is one edit, not N.

python
class Meta:
    permissions = [("codename", "Human-readable name")]

user.has_perm("app_label.codename")
user.groups.add(group)

What we're doing: Add a custom "can_publish_article" permission and grant it to an Editors group, rather than to individual users.

articles/models.pypython
class Article(models.Model):
    title = models.CharField(max_length=200)
    status = models.CharField(max_length=20, default="draft")

    class Meta:
        permissions = [("can_publish_article", "Can publish article")]
5
permissions is a list of (codename, description) tuples — makemigrations picks this up like any other model change and creates it via a migration.

Why this works: Publishing is a business action with real consequences (making an article publicly visible) distinct from merely "changing" the article — a custom permission lets it be granted independently of the generic change_article permission, so a contributor can edit drafts without being able to publish them.

Granting a new business-action permission to individual users one at a time instead of via a Group

Wrong

python
perm = Permission.objects.get(codename="can_publish_article")
for user in User.objects.filter(department="editorial"):
    user.user_permissions.add(perm)   # 40 individual grants, no shared record

Better

python
editors, _ = Group.objects.get_or_create(name="Editors")
editors.permissions.add(Permission.objects.get(codename="can_publish_article"))
for user in User.objects.filter(department="editorial"):
    user.groups.add(editors)   # revoking/adding permissions later touches ONE group

What you see: Revoking or changing the publish permission later means updating every individual user's permission set one at a time, with no single place that represents "what an editor can do" — a new hire needs the same manual per-permission grant repeated, and it is easy to miss one.

Why: Individual per-user permission grants and a Group both end up checked the same way by has_perm() — the difference is entirely about MAINTAINING the assignment over time. A Group centralizes "what a role can do" into one editable record; direct per-user grants scatter that same information across every user row, which is the exact N-times-the-work problem RBAC exists to avoid.

A custom permission, granted via a Group rather than per-user
editors.permissions.add(...)one grant,many users

can_publish_article

Meta.permissions

Editors (Group)

a reusable, named bundle

40 users

user.groups.add(editors)

  • can_publish_article — Meta.permissions
    • leads to Editors (Group) (editors.permissions.add(...))
  • Editors (Group) — a reusable, named bundle
    • leads to 40 users (one grant, many users)
  • 40 users — user.groups.add(editors)

The permission layers, narrowest to broadest scope

The permission layers, narrowest to broadest scope
LayerAnswers
Auto-created model permission"can this user add/change/delete/view this MODEL at all"
Custom permission (Meta.permissions)"can this user perform this specific BUSINESS action"
Group"grant a reusable bundle of the above to many users at once"

Together

python
class Article(models.Model):
    class Meta:
        permissions = [("can_publish_article", "Can publish article")]

editors = Group.objects.create(name="Editors")
editors.permissions.add(Permission.objects.get(codename="can_publish_article"))
user.groups.add(editors)
user.has_perm("articles.can_publish_article")   # True, via the group

Remember: Every model auto-gets add/change/delete/view permissions; Meta.permissions declares custom business-action ones. A Group is a reusable, named bundle of Permissions — the RBAC pattern in miniature. has_perm() checks direct permissions AND every group a user belongs to. Model permissions answer "can this user act on this MODEL at all" — never "on which specific instance," which needs a separate object-level check.

See also: object level and resource level authorization · passwords staff and permissions · inlines actions and permissions

Advertisement

Object-level and resource-level authorization

Ownership rules, and where DRF and admin permissions fit relative to Django's own.

Object-level authorization, ownership, and where DRF/admin permissions fit

coreadvanced

Object-level authorization answers "can this user act on THIS SPECIFIC instance," a question the built-in permission system (model-level only) can't answer on its own — Django's has_perm(perm, obj) accepts an optional object argument specifically for this, though the default ModelBackend ignores it (returns based on the model-level permission alone); a real object-level check needs either explicit code (article.author == request.user) or a third-party backend (django-guardian) that actually implements per-object permissions. Ownership rules are the most common concrete case: "a user may edit an Order only if they created it." Resource-level permissions generalize past "one object" to "this whole category of resource for this tenant/team." DRF permission classes (IsAuthenticated, a custom has_object_permission()) are the same object-level pattern applied at the API layer; admin permissions layer has_*_permission() on top for the admin UI specifically.

Think of it as

has_perm(perm, obj) accepting an object parameter is a real, deliberate extension point in Django's auth system — but the DEFAULT ModelBackend simply ignores the obj argument and answers purely from the model-level permission, because Django has no built-in concept of object ownership to check against. This is not an oversight; it is Django declining to guess at a business rule ("owns," "assigned to," "belongs to their team") that varies completely between applications. The practical consequence is that "object-level authorization" in most real Django projects is just explicit code at the point of use (an equality check, a queryset filter) rather than a call into the permission SYSTEM — django-guardian exists specifically to make has_perm(perm, obj) actually do something for projects that want the check to look uniform even though the underlying rule is still project-specific. DRF's permission classes mirror this exactly at the API layer: has_permission() is the model-level-style check (is this endpoint reachable at all), while has_object_permission() is the object-level one, called only after a specific object is retrieved — the same two-tier shape as model permissions vs. explicit ownership checks, just named differently.

python
def has_object_permission(self, request, view, obj):
    return obj.owner_id == request.user.id

What we're doing: A DRF view where any authenticated user can list their own orders, but editing a specific order requires actually owning it — combining a queryset filter with an object-level permission.

orders/views.pypython
class OrderViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, IsOwner]
    serializer_class = OrderSerializer

    def get_queryset(self):
        return Order.objects.filter(customer=self.request.user)

class IsOwner(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.customer == request.user
6
get_queryset() means another customer's order never appears in a list response at all — the first, broader layer.
9
has_object_permission() is the second layer — even if an order id were guessed and requested directly, ownership is still checked before allowing the action.

Why this works: Relying on the queryset filter alone would still be safe here since DRF's generic views fetch detail objects through get_queryset() too — but layering an explicit has_object_permission() check makes the ownership RULE visible and enforced at the permission layer, not just an incidental effect of how the queryset happens to be filtered.

Adding has_object_permission() but forgetting it is never called for list actions

Wrong

python
class OrderViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, IsOwner]   # IsOwner has has_object_permission only
    queryset = Order.objects.all()   # NOT filtered — every customer's orders

Better

python
class OrderViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, IsOwner]

    def get_queryset(self):
        return Order.objects.filter(customer=self.request.user)

What you see: The list endpoint (GET /orders/) returns every customer's orders, not just the requesting user's — while the detail endpoint (GET /orders/5/) correctly blocks access to another customer's order, because has_object_permission() IS called there.

Why: DRF only calls has_object_permission() for actions that operate on a single, already-retrieved object (retrieve/update/destroy) — list actions never fetch an individual object to check, so an object-level permission class provides zero protection for a list endpoint on its own. get_queryset() filtering is the actual, documented mechanism for restricting a list.

Two layers, narrowest question last

get_queryset() filter

another customer's order never appears in a list at all

has_object_permission()

checked only once a specific object is retrieved

  1. get_queryset() filter — another customer's order never appears in a list at all
  2. has_object_permission() — checked only once a specific object is retrieved

Model-level vs object-level, Django and DRF side by side

Model-level vs object-level, Django and DRF side by side
LayerDjangoDRF
"Can this user act on this TYPE at all"has_perm("app.change_x") (model-level)has_permission()
"Can this user act on THIS instance"explicit code, or django-guardianhas_object_permission()

Together

python
class IsOwner(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.owner == request.user

Remember: has_perm(perm, obj) exists but the default backend ignores obj — real object-level authorization in Django means explicit code (or a backend like django-guardian), not a permission-system call. DRF splits the same idea into has_permission() (view-level) and has_object_permission() (never called for list actions — use get_queryset() filtering there). Admin permissions are a separate access surface from application-level authorization.

See also: model permissions and groups · inlines actions and permissions

Advertisement