Django Ninja Extra Permissions Guide
Permissions in Django Ninja Extra provide a flexible way to control access to your API endpoints. The permission system is inspired by Django REST Framework and allows you to define both global and endpoint-specific access controls.
How Permissions Work
Permissions are checked at the start of each route function execution. They use the authentication information available in request.user
and request.auth
to determine if the request should be allowed to proceed.
Built-in Permission Classes
Django Ninja Extra comes with several built-in permission classes:
1. AllowAny
Allows unrestricted access to any endpoint.
from ninja_extra import permissions, api_controller, http_get
@api_controller(permissions=[permissions.AllowAny])
class PublicController:
@http_get("/public")
def public_endpoint(self):
return {"message": "This endpoint is public"}
2. IsAuthenticated
Only allows access to authenticated users.
from ninja_extra import permissions, api_controller, http_get
@api_controller(permissions=[permissions.IsAuthenticated])
class PrivateController:
@http_get("/profile")
def get_profile(self, request):
return {
"username": request.user.username,
"email": request.user.email
}
3. IsAuthenticatedOrReadOnly
Allows read-only access to unauthenticated users, but requires authentication for write operations.
from ninja_extra import permissions, api_controller, http_get, http_post
@api_controller("/posts", permissions=[permissions.IsAuthenticatedOrReadOnly])
class BlogController:
@http_get("/") # Accessible to everyone
def list_posts(self):
return {"posts": ["Post 1", "Post 2"]}
@http_post("/") # Only accessible to authenticated users
def create_post(self, request, title: str):
return {"message": f"Post '{title}' created by {request.user.username}"}
4. IsAdminUser
Only allows access to admin users (users with is_staff=True
).
from ninja_extra import permissions, api_controller, http_get
@api_controller("/admin", permissions=[permissions.IsAdminUser])
class AdminController:
@http_get("/stats")
def get_stats(self):
return {"active_users": 100, "total_posts": 500}
Custom Permissions
You can create custom permissions by subclassing BasePermission
:
from ninja_extra import permissions, api_controller, http_get
from django.http import HttpRequest
class HasAPIKey(permissions.BasePermission):
def has_permission(self, request: HttpRequest, controller):
api_key = request.headers.get('X-API-Key')
return api_key == 'your-secret-key'
@api_controller(permissions=[HasAPIKey])
class APIKeyProtectedController:
@http_get("/protected")
def protected_endpoint(self):
return {"message": "Access granted with valid API key"}
Object-Level Permissions
For fine-grained control over individual objects:
from ninja_extra import permissions, api_controller, http_get
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from .models import Post
class IsPostAuthor(permissions.BasePermission):
def has_object_permission(self, request: HttpRequest, controller, obj: Post):
return obj.author == request.user
@api_controller("/posts")
class PostController:
@http_get("/{post_id}", permissions=[permissions.IsAuthenticated & IsPostAuthor()])
def get_post(self, request, post_id: int):
# The has_object_permission method will be called automatically
# when using get_object_or_exception or get_object_or_none
post = self.get_object_or_exception(Post, id=post_id)
return {"title": post.title, "content": post.content}
Combining Permissions
Django Ninja Extra supports combining permissions using logical operators:
&
(AND): Both permissions must pass|
(OR): At least one permission must pass~
(NOT): Inverts the permission
from ninja_extra import permissions, api_controller, http_get
class HasPremiumSubscription(permissions.BasePermission):
def has_permission(self, request, controller):
return request.user.has_perm('premium_subscription')
@api_controller("/content")
class ContentController:
@http_get("/basic", permissions=[permissions.IsAuthenticated | HasPremiumSubscription()])
def basic_content(self):
return {"content": "Basic content"}
@http_get("/premium", permissions=[permissions.IsAuthenticated & HasPremiumSubscription()])
def premium_content(self):
return {"content": "Premium content"}
@http_get("/non-premium", permissions=[permissions.IsAuthenticated & ~HasPremiumSubscription()])
def non_premium_content(self):
return {"content": "Content for non-premium users"}
Role-Based Permissions
You can dynamically check different roles or permissions for a user using a single permission class. Here's an example:
from ninja_extra import permissions, api_controller, http_get, http_post, http_delete
class HasRole(permissions.BasePermission):
def __init__(self, required_role: str):
self.required_role = required_role
def has_permission(self, request, controller):
return request.user.has_perm(self.required_role)
@api_controller("/articles", permissions=[permissions.IsAuthenticated])
class ArticleController:
@http_get("/", permissions=[HasRole("articles.view")])
def list_articles(self):
return {"articles": ["Article 1", "Article 2"]}
@http_post("/", permissions=[HasRole("articles.add")])
def create_article(self, title: str):
return {"message": f"Article '{title}' created"}
@http_delete("/{id}", permissions=[HasRole("articles.delete")])
def delete_article(self, id: int):
return {"message": f"Article {id} deleted"}
HasRole
permission class is used to check if the user has the articles.view
, articles.add
or articles.delete
permission in different routes.
Interacting with Route Function Parameters and RouteContext
Sometimes you need to access route function parameters within your permission class before the actual route function is executed. Django Ninja Extra provides the RouteContext
class to handle this scenario.
By default, permission checks are performed before route function parameters are resolved. However, you can explicitly trigger parameter resolution using the RouteContext
class.
Basic Route Context Usage
from ninja_extra import permissions, api_controller, http_get, ControllerBase
from django.http import HttpRequest
class IsOwner(permissions.BasePermission):
def has_permission(self, request: HttpRequest, controller: ControllerBase):
# Access route context and compute parameters
controller.context.compute_route_parameters()
# Now you can access path and query parameters
user_id = controller.context.kwargs.get('user_id')
return request.user.id == user_id
@api_controller("/users")
class UserController:
@http_get("/{user_id}/profile", permissions=[IsOwner()])
def get_user_profile(self, user_id: int):
return {"message": f"Access granted to profile {user_id}"}
Advanced Route Context Examples
Here are more complex examples showing different ways to use route context:
from ninja_extra import permissions, api_controller, http_get, http_post, ControllerBase
from django.http import HttpRequest
from typing import Optional
class HasTeamAccess(permissions.BasePermission):
def has_permission(self, request: HttpRequest, controller: ControllerBase):
# Compute parameters to access both path and query parameters
controller.context.compute_route_parameters()
team_id = controller.context.kwargs.get('team_id')
role = controller.context.kwargs.get('role', 'member') # Default to 'member'
return request.user.has_team_permission(team_id, role)
class HasProjectAccess(permissions.BasePermission):
def __init__(self, required_role: str):
self.required_role = required_role
def has_permission(self, request: HttpRequest, controller: ControllerBase):
controller.context.compute_route_parameters()
# Access multiple parameters
project_id = controller.context.kwargs.get('project_id')
team_id = controller.context.kwargs.get('team_id')
return (
request.user.is_authenticated and
request.user.has_project_permission(project_id, team_id, self.required_role)
)
@api_controller("/teams")
class TeamProjectController:
@http_get("/{team_id}/projects/{project_id}", permissions=[HasTeamAccess() & HasProjectAccess("viewer")])
def get_project(self, team_id: int, project_id: int):
return {"message": f"Access granted to project {project_id} in team {team_id}"}
@http_post("/{team_id}/projects", permissions=[HasTeamAccess() & HasProjectAccess("admin")])
def create_project(self, team_id: int, name: str, description: Optional[str] = None):
return {
"message": f"Created project '{name}' in team {team_id}",
"description": description
}
Working with Query Parameters
You can also access query parameters in your permission classes:
from ninja_extra import permissions, api_controller, http_get, ControllerBase
from django.http import HttpRequest
class HasFeatureAccess(permissions.BasePermission):
def has_permission(self, request: HttpRequest, controller: ControllerBase):
controller.context.compute_route_parameters()
# Access query parameters
feature_name = controller.context.kwargs.get('feature')
environment = controller.context.kwargs.get('env', 'production')
return request.user.has_feature_access(feature_name, environment)
@api_controller("/features")
class FeatureController:
@http_get("/check", permissions=[HasFeatureAccess()])
def check_feature(self, feature: str, env: str = "production"):
return {
"feature": feature,
"environment": env,
"status": "enabled"
}
Important Notes
- Always call
compute_route_parameters()
before accessing route parameters in permission classes - Route parameters are available in
controller.context.kwargs
after computation - Both path parameters and query parameters are accessible
- You can combine route context-based permissions with other permission types
- Route parameters are computed only once, even if accessed by multiple permission classes
- The computation results are cached for the duration of the request