Skip to content

DRF Authentication and Permissions

When you build APIs with Django REST Framework (DRF), you need a way to check who the user is (authentication) and what they are allowed to do (permissions). Without this, anyone could read or change your data. DRF gives you a flexible system to handle both of these things. In this section, we will look at different ways to authenticate users and control access using permission classes, and how to use them in your API views.

Authentication means checking “who is this user?” You can build this yourself by writing custom authentication classes, but most people use a library called djoser instead. Djoser is a popular package that already comes with ready-made views for user registration, login, logout, password reset, and more. It works well with DRF and saves you a lot of time, so you don’t have to write all this code from scratch.

Official documentation: https://djoser.readthedocs.io/en/latest/

  1. Install Djoser:

    Terminal window
    uv add djoser djangorestframework-simplejwt
  2. Add Djoser to your INSTALLED_APPS in settings.py:

    INSTALLED_APPS = [
    # other apps
    'djoser',
    'rest_framework_simplejwt',
    ]
  3. Include Djoser’s URLs in your project’s urls.py:

    from django.urls import path, include
    urlpatterns = [
    # other urls
    path('auth/', include('djoser.urls')),
    path('auth/', include('djoser.urls.jwt')), # for JWT authentication
    ]
  4. Tell DRF to use JWT authentication in settings.py:

    REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    }
  5. Set up Simple JWT settings in settings.py:

    You can change how long tokens last, whether they rotate, and other settings. Here’s an example:

    from datetime import timedelta
    SIMPLE_JWT = {
    'AUTH_HEADER_TYPES': ('JWT',),
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
    "ROTATE_REFRESH_TOKENS": False,
    "BLACKLIST_AFTER_ROTATION": False,
    "UPDATE_LAST_LOGIN": False,
    }
  6. Run migrations to create the database tables you need:

    Terminal window
    uv run python manage.py makemigrations
    uv run python manage.py migrate

Djoser already gives you default serializers for things like user registration and login. But you can change these serializers to add more fields or change how authentication behaves. For example, you can make a custom serializer for registration that also asks for first_name and last_name:

# serializers.py
from djoser.serializers import UserCreateSerializer as BaseUserCreateSerializer, UserSerializer as BaseUserSerializer
class UserCreateSerializer(BaseUserCreateSerializer):
class Meta(BaseUserCreateSerializer.Meta):
fields = BaseUserCreateSerializer.Meta.fields + ('first_name', 'last_name')
class UserSerializer(BaseUserSerializer):
class Meta(BaseUserSerializer.Meta):
fields = BaseUserSerializer.Meta.fields + ('first_name', 'last_name')

Now tell Djoser to use your custom serializer in settings.py:

Docs: https://djoser.readthedocs.io/en/latest/settings.html#serializers

DJOSER = {
'SERIALIZERS': {
'user_create': 'your_app.serializers.UserCreateSerializer',
'current_user': 'your_app.serializers.UserSerializer',
},
}
Example of User and Profile Models
# models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser): # You can add additional fields here if needed
pass
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
dob = models.DateField(null=True, blank=True)
bio = models.TextField(null=True, blank=True)
def __str__(self):
return f"{self.user.username}'s Profile"
# serializers.py
from rest_framework import serializers
from .models import User, Profile
# user serializer is created by djoser, we will create a profile serializer
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ['dob', 'bio']
# we dont need to include the user field here because
# we will set it in the view using the request.user
# and this api is only for the authenticated user
def create(self, validated_data):
user = self.context['request'].user
profile = Profile.objects.create(user=user, **validated_data)
return profile
# views.py
from rest_framework import viewsets
from .models import Profile
from .serializers import ProfileSerializer
from rest_framework.permissions import IsAuthenticated
class ProfileViewSet(viewsets.ModelViewSet):
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
# return only the profile of the authenticated user
return self.queryset.filter(user=self.request.user)
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context

JWT (JSON Web Token) is a small, safe-to-put-in-a-URL piece of text that carries information between two sides - usually between your app (client) and your server. It is widely used for login and access control in web apps. A JWT has three parts: a header, a payload, and a signature.

  • The header tells you what type of token it is and which method was used to sign it.
  • The payload holds the actual information (called “claims”), usually about the user, plus any extra data you want to include.
  • The signature proves that nobody changed the token after it was created.

Here’s how it works in simple terms: when a user logs in, the server creates a JWT that holds the user’s info and sends it back to them. The client then attaches this token to every future request, inside the Authorization header. The server checks the token’s signature, reads the user’s info from it, and decides if the request should be allowed.

If you want to see what’s actually inside a JWT, you can use a free online tool like jwt.io to break it down and look at the contents.

JWT Debugger lets you paste a JWT token and see its header, payload, and signature in a format that’s easy to read. This is a great way to understand what data is being stored inside the token and how it’s put together.

Djoser gives you a ready-made set of API endpoints (URLs) for handling users - things like signing up, logging in, and managing accounts.

Docs: https://djoser.readthedocs.io/en/latest/getting_started.html#available-endpoints

  • /users/ (POST for registration, GET for listing users)
  • /users/me/(GET for current user details, PUT/PATCH for updating current user)
  • /users/resend_activation/ (POST for resending activation email)
  • /users/set_password/ (POST for setting password)
  • /users/reset_password/ (POST for resetting password)
  • /users/reset_password_confirm/ (POST for confirming password reset)
  • /users/set_username/ (POST for setting username)
  • /users/reset_username/ (POST for resetting username)
  • /users/reset_username_confirm/ (POST for confirming username reset)
  • /token/login/ (Token Based Authentication)
  • /token/logout/ (Token Based Authentication)
  • /jwt/create/ (JSON Web Token Authentication)
  • /jwt/refresh/ (JSON Web Token Authentication)
  • /jwt/verify/ (JSON Web Token Authentication)
Dojser Default API Endpoints in Detail
  1. POST /api/auth/jwt/create/

    Description: Log in and get an access token plus a refresh token

    • Request

      {
      "email": "user@example.com",
      "password": "password"
      }
    • Response

      {
      "access": "access_token",
      "refresh": "refresh_token"
      }
    • Access

      • Public
  2. POST /api/auth/jwt/refresh/

    Description: Get a new access token

    • Request

      {
      "refresh": "refresh_token"
      }
    • Response

      {
      "access": "new_access_token"
      }
    • Access

      • Public (you need a valid refresh token)
  3. POST /api/auth/jwt/verify/

    Description: Check if a token is still valid

    • Request

      {
      "token": "access_token"
      }
    • Response

      • 200 OK (valid)
      • 401 Unauthorized (invalid)
    • Access

      • Public
  4. GET /api/auth/users/

    Description: List all users

    • Response

      [
      {
      "id": 1,
      "email": "user@example.com"
      }
      ]
    • Access

      • Admin only (you must restrict this yourself)
  5. POST /api/auth/users/

    Description: Sign up a new user

    • Request

      {
      "email": "user@example.com",
      "password": "password"
      }
    • Response

      {
      "id": 1,
      "email": "user@example.com"
      }
    • Access

      • Public
  6. GET /api/auth/users/{id}/

    Description: Get a user by their ID

    • Response

      {
      "id": 1,
      "email": "user@example.com"
      }
    • Access

      • Admin or the user themself (Owner)
  7. PUT /api/auth/users/{id}/

    Description: Fully update a user

    • Request

      {
      "email": "new@example.com"
      }
    • Access

      • Admin or the user themself (Owner)
  8. PATCH /api/auth/users/{id}/

    Description: Partly update a user (only some fields)

    • Request

      {
      "email": "updated@example.com"
      }
    • Access

      • Admin or the user themself (Owner)
  9. DELETE /api/auth/users/{id}/

    Description: Delete a user

    • Response

      • 204 No Content
    • Access

      • Admin or the user themself (Owner)
  10. POST /api/auth/users/activation/

    Description: Activate an account using a token sent by email

    • Request

      {
      "uid": "encoded_user_id",
      "token": "activation_token"
      }
    • Response

      • 204 No Content
    • Access

      • Public (you just need a valid token)
  11. GET /api/auth/users/me/

    Description: Get the details of the currently logged-in user

    • Headers

      Authorization: Bearer <access_token>
    • Response

      {
      "id": 1,
      "email": "user@example.com"
      }
    • Access

      • Authenticated user (must be logged in)
  12. PUT /api/auth/users/me/

    Description: Fully update the currently logged-in user

    • Request

      {
      "email": "new@example.com"
      }
    • Access

      • Authenticated user (must be logged in)
  13. PATCH /api/auth/users/me/

    Description: Partly update the currently logged-in user

    • Request

      {
      "email": "updated@example.com"
      }
    • Access

      • Authenticated user (must be logged in)
  14. DELETE /api/auth/users/me/

    Description: Delete your own account

    • Response

      • 204 No Content
    • Access

      • Authenticated user (must be logged in)
  15. POST /api/auth/users/resend_activation/

    Description: Send the activation email again

    • Request

      {
      "email": "user@example.com"
      }
    • Response

      • 204 No Content
    • Access

      • Public
  16. POST /api/auth/users/reset_email/

    Description: Ask to change your email

    • Request

      {
      "email": "new@example.com"
      }
    • Response

      • 204 No Content
    • Access

      • Authenticated user (must be logged in)
  17. POST /api/auth/users/reset_email_confirm/

    Description: Confirm the email change

    • Request

      {
      "uid": "encoded_user_id",
      "token": "email_reset_token"
      }
    • Response

      • 204 No Content
    • Access

      • Public (you just need a valid token)
  18. POST /api/auth/users/reset_password/

    Description: Ask to reset your password

    • Request

      {
      "email": "user@example.com"
      }
    • Response

      • 204 No Content
    • Access

      • Public
  19. POST /api/auth/users/reset_password_confirm/

    Description: Confirm the password reset

    • Request

      {
      "uid": "encoded_user_id",
      "token": "reset_token",
      "new_password": "newpassword"
      }
    • Response

      • 204 No Content
    • Access

      • Public (you just need a valid token)
  20. POST /api/auth/users/set_email/

    Description: Change your email while you’re logged in

    • Request

      {
      "email": "new@example.com"
      }
    • Access

      • Authenticated user (must be logged in)
  21. POST /api/auth/users/set_password/

    Description: Change your password while you’re logged in

    • Request

      {
      "current_password": "oldpassword",
      "new_password": "newpassword"
      }
    • Response

      • 204 No Content
    • Access

      • Authenticated user (must be logged in)
Reference URLs for below examples
# urls.py
from django.urls import path, include
urlpatterns = [
# other urls
path('auth/', include('djoser.urls')),
path('auth/', include('djoser.urls.jwt')), # for JWT authentication
]

First, let’s sign up a user using the /users/ endpoint. We send a POST request with the user’s details, and it creates a new account.

Terminal window
curl -X POST http://localhost:8000/auth/users/ \
-H "Content-Type: application/json" \
-d '{
"username": "john_doe",
"email": "john_doe@example.com",
"password": "secure_password"
}'

This creates a new user with the username john_doe, email john_doe@example.com, and password secure_password.

To log in, we use the /jwt/create/ endpoint to get a JWT token. We send a POST request with the user’s username and password.

Terminal window
curl -X POST http://localhost:8000/auth/jwt/create/ \
-H "Content-Type: application/json" \
-d '{
"username": "john_doe",
"password": "secure_password"
}'

This gives back a JSON response with the access token and refresh token:

{
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
  • access token: You use this to prove who you are on every request you make after logging in. It usually has a short lifespan (e.g. 5 minutes) to stay safe.
  • refresh token: You use this to get a brand new access token once the old one expires. It usually lasts longer (e.g. 1 day) and lets the user stay “logged in” without typing their password again.

Getting a New Access Token using the Refresh Token

Section titled “Getting a New Access Token using the Refresh Token”

Once the access token expires, you don’t need to ask the user to log in again. Instead, send the refresh token to the /jwt/refresh/ endpoint, and you’ll get a brand new access token back.

Terminal window
curl -X POST http://localhost:8000/auth/jwt/refresh/ \
-H "Content-Type: application/json" \
-d '{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}'

This returns a new access token:

{
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}

To see the profile of the user who is currently logged in, send a GET request to the /users/me/ endpoint with the access token in the Authorization header.

Terminal window
curl -X GET http://localhost:8000/auth/users/me/ \
-H "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" \
-H "Content-Type: application/json"

If you’re testing this in a browser, you can use an extension like ModHeader to add the Authorization header with the JWT token. ModHeader for Chrome

Sometimes the standard CRUD operations (Create, Read, Update, Delete) are not enough. You can add your own custom actions to a viewset to handle something extra. For example, you might want users to be able to change their password through a special endpoint.

Reference code for custom action
# models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Customer(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE) # other fields
def __str__(self) -> str:
return self.user.username
# serializers.py
from rest_framework import serializers
from .models import Customer
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = [
'user',
# other fields
]
# views.py
from rest_framework import viewsets
from .models import Customer
class CustomerViewSet(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import CustomerViewSet
router = DefaultRouter()
router.register(r'customers', CustomerViewSet)
urlpatterns = [
path('', include(router.urls)),
]

Now let’s say we want an endpoint like /customers/me/ that lets a logged-in user see their own customer profile. We can do this with a custom action inside CustomerViewSet:

from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework import viewsets
from .models import Customer
from rest_framework.response import Response
class CustomerViewSet(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
@action(detail=False, methods=['GET', 'PUT'], permission_classes=[IsAuthenticated])
def me(self, request):
customer, _ = Customer.objects.get_or_create(user=request.user)
if request.method == 'GET':
serializer = self.get_serializer(customer)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = self.get_serializer(customer, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)

Let’s break down what’s happening in this code:

  • detail=False means this action is not about one specific item (it doesn’t need an ID/primary key in the URL). If we used detail=True instead, the URL would become /customers/{pk}/me/, which is not what we want here.

  • methods=['GET', 'PUT'] means this action can handle both GET and PUT requests. GET is used to fetch the customer’s profile, and PUT is used to update it.

  • permission_classes=[IsAuthenticated] makes sure only logged-in users can use this endpoint. You can set different permissions on a single action, and the rest of the viewset can have its own permissions too. Whatever permission you set on the action will override the viewset’s permission, but only for that specific action.

Permissions decide who is allowed to do what in your API.

This is one of the most important things to understand properly, so let’s go slow and build it up step by step.

Think of authentication and permissions as two separate questions:

  • Authentication asks: “Who are you?” (are you logged in, and if yes, which user are you?)
  • Permissions ask: “Now that I know who you are, what are you allowed to do?”

In Django REST Framework (DRF), permission classes help you answer questions like:

  • Is the user logged in at all?
  • Is the user an admin?
  • Is this particular user allowed to edit this particular object (for example, can John edit Mary’s post)?

This is what keeps your API safe from people doing things they shouldn’t be able to do.

DRF comes with several ready-made permission classes, so you don’t always have to build your own:

  • AllowAny: Anyone can use this endpoint, even people who are not logged in.
  • IsAuthenticated: Only people who are logged in can use it.
  • IsAdminUser: Only admin/staff accounts can use it.
  • IsAuthenticatedOrReadOnly: Anyone can read (view) the data, but only logged-in users can create, update, or delete it.
  • DjangoModelPermissions: Uses Django’s own model permission system (add, change, delete, and sometimes view).
  • DjangoObjectPermissions: Like the one above, but checks permission on a specific object, not just the model in general.
  • Custom permissions: If none of the above fit your need, you can write your own rules by extending BasePermission.

Global permissions are the default rule that applies to your whole API. You set this once, in settings.py.

This default will apply to every view, unless you specifically change it for one view.

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
# or 'rest_framework.permissions.IsAdminUser'
# or 'rest_framework.permissions.AllowAny'
# or any other permission class
],
}

In the example above, every single endpoint in your project will require the user to be logged in, by default.

If just one endpoint needs different rules than the rest, you can set permission_classes directly on that view or viewset.

This will override the global default, but only for that one view.

from rest_framework.permissions import IsAuthenticated
from rest_framework import viewsets
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
# Only authenticated users can access this view
permission_classes = [IsAuthenticated]

Different Permission for Different HTTP Methods

Section titled “Different Permission for Different HTTP Methods”

Sometimes you want different rules for the same endpoint depending on the type of request. For example:

  • Anyone can read (view) products using GET
  • Only logged-in users can change products using POST, PUT, PATCH, or DELETE

You can set this up by overriding the get_permissions() method.

from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework import viewsets
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def get_permissions(self):
if self.request.method in ['POST', 'PUT', 'PATCH', 'DELETE']:
return [IsAuthenticated()]
return [AllowAny()]

This is a very common, beginner-friendly setup: anyone can read your data, but only logged-in users can change it.

If the built-in permissions don’t cover what you need, you can write your own permission class.

There are two main methods you can add:

  • has_permission() - for checking access at the view level (a general check before anything else happens)
  • has_object_permission() - for checking access to one specific object (for example, “is this user the owner of this exact post?”)
# permissions.py
from rest_framework import permissions
from rest_framework.permissions import BasePermission
class IsOwner(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
class IsAdminOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user and request.user.is_staff

Here we made two custom permissions:

  • IsOwner: only the person who owns the object can access it.
  • IsAdminOrReadOnly: everyone can read the data, but only staff/admin users can change it.

DRF can also plug into Django’s own built-in model permission system, instead of you writing everything from scratch.

DjangoModelPermissions checks the user’s Django model permissions before letting them make any changes.

In simple words, DRF is basically asking: “Does this user actually have permission to do this action on this model?”

The common checks are:

  • add
  • change
  • delete
  • (and view, only if you set it up for GET requests)
  • This permission requires the user to be logged in for every request, even GET requests.

This is useful if you’re already managing permissions through Django’s admin panel, with users and groups.

from rest_framework.permissions import DjangoModelPermissions
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [DjangoModelPermissions]

Django Model Permissions Or Anon Read Only

Section titled “Django Model Permissions Or Anon Read Only”

Use DjangoModelPermissionsOrAnonReadOnly when you want this exact setup:

  • Users who are not logged in (anonymous users): can only read the data
  • Users who are logged in: get checked against model permissions before they can write (change) anything

This permission class already comes built into DRF, so you can just plug it in directly - no extra setup needed.

from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
from rest_framework import viewsets
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]

Use this approach when you want to create your own, fully custom access rules from scratch.

Here’s the simple flow to follow:

  1. Add your custom permission inside the model’s Meta class.
  2. Run migrations.
  3. Give that permission to users or groups.
  4. Use a custom DRF permission class in your view to check for it.

1. Create Permission in Model (Meta class)

Section titled “1. Create Permission in Model (Meta class)”
# models.py
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
permissions = [
('can_publish_product', 'Can publish product'),
]
Terminal window
uv run python manage.py makemigrations
uv run python manage.py migrate

3. Create Group and Assign Permissions to Users

Section titled “3. Create Group and Assign Permissions to Users”

You can do this in two beginner-friendly ways.

Admin panel way:

  1. Open Django admin and go to Groups.
  2. Create a group (example: ProductManagers).
  3. Add permissions to that group (example: view_product, change_product, can_publish_product).
  4. Open a user and add that user to the group.

Shell way:

from django.contrib.auth.models import Group, Permission
from django.contrib.auth import get_user_model
User = get_user_model()
# 1) Create or get group
group, _ = Group.objects.get_or_create(name='ProductManagers')
# 2) Add permissions to group
group.permissions.add(
Permission.objects.get(codename='view_product'),
Permission.objects.get(codename='change_product'),
Permission.objects.get(codename='can_publish_product'),
)
# 3) Add user to group
user = User.objects.get(username='john_doe')
user.groups.add(group)
# permissions.py
from rest_framework.permissions import BasePermission
class CanPublishProduct(BasePermission):
message = 'You do not have permission to publish products.'
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
# Format: app_label.permission_codename
return request.user.has_perm('store.can_publish_product')

Now use it in your viewset:

from rest_framework import viewsets
from .permissions import CanPublishProduct
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [CanPublishProduct]

Control Existing Permissions by HTTP Method

Section titled “Control Existing Permissions by HTTP Method”

Use this pattern when you want to reuse Django’s already-existing model permissions (view, add, change, delete), but check a different one depending on which HTTP method is being used.

Here’s the simple idea:

  • GET/HEAD/OPTIONS -> needs view_product
  • POST -> needs add_product
  • PUT/PATCH -> needs change_product
  • DELETE -> needs delete_product

This gives you fine control over access, without having to invent brand new permission names.

# permissions.py
# it is a generic permission but you can customize the perms_map to fit your needs
from rest_framework import permissions
class ViewProductPermission(permissions.BasePermission):
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
perms_by_method = {
'GET': 'store.view_product',
'HEAD': 'store.view_product',
'OPTIONS': 'store.view_product',
'POST': 'store.add_product',
'PUT': 'store.change_product',
'PATCH': 'store.change_product',
'DELETE': 'store.delete_product',
}
required_perm = perms_by_method.get(request.method)
if not required_perm:
return False
# Format: app_label.permission_codename
return request.user.has_perm(required_perm)