Permissions¶
Django-Bolt uses "guards" to control access to endpoints. Guards are permission checks that run in Rust after authentication but before your handler is called.
Built-in guards¶
IsAuthenticated¶
Requires a valid authentication token:
from django_bolt.auth import JWTAuthentication, IsAuthenticated
@api.get("/profile", auth=[JWTAuthentication()], guards=[IsAuthenticated()])
async def profile(request):
return {"user_id": request.user.id}
Returns 401 Unauthorized if authentication fails.
IsAdminUser¶
Requires the user to be a superuser:
from django_bolt.auth import IsAdminUser
@api.delete("/admin/users/{user_id}", auth=[JWTAuthentication()], guards=[IsAdminUser()])
async def delete_user(user_id: int):
# Only superusers can access this
return {"deleted": user_id}
Returns 403 Forbidden if the user is not a superuser.
IsStaff¶
Requires the user to have staff status:
from django_bolt.auth import IsStaff
@api.get("/admin/dashboard", auth=[JWTAuthentication()], guards=[IsStaff()])
async def admin_dashboard():
return {"dashboard": "staff only"}
HasPermission¶
Requires a specific Django permission:
from django_bolt.auth import HasPermission
@api.post("/articles", auth=[JWTAuthentication()], guards=[HasPermission("blog.add_article")])
async def create_article():
return {"created": True}
Permission strings follow Django's format: app_label.permission_codename.
HasAnyPermission¶
Requires at least one of the specified permissions (OR logic):
from django_bolt.auth import HasAnyPermission
@api.get(
"/content",
auth=[JWTAuthentication()],
guards=[HasAnyPermission(["blog.view_article", "blog.add_article"])]
)
async def view_content():
return {"content": "visible"}
HasAllPermissions¶
Requires all specified permissions (AND logic):
from django_bolt.auth import HasAllPermissions
@api.delete(
"/articles/{id}",
auth=[JWTAuthentication()],
guards=[HasAllPermissions(["blog.delete_article", "blog.change_article"])]
)
async def delete_article(id: int):
return {"deleted": id}
AllowAny¶
Explicitly allows any request, bypassing authentication:
from django_bolt.auth import AllowAny
@api.get("/public", guards=[AllowAny()])
async def public():
return {"message": "Anyone can see this"}
Combining guards¶
Use multiple guards for layered security:
@api.post(
"/admin/settings",
auth=[JWTAuthentication()],
guards=[IsAuthenticated(), IsStaff(), HasPermission("core.change_settings")]
)
async def update_settings():
return {"updated": True}
Guards are checked in order. The request is rejected as soon as any guard fails.
Error responses¶
Guards return appropriate HTTP status codes:
| Guard | Failure Status |
|---|---|
IsAuthenticated |
401 Unauthorized |
IsAdminUser |
403 Forbidden |
IsStaff |
403 Forbidden |
HasPermission |
403 Forbidden |
HasAnyPermission |
403 Forbidden |
HasAllPermissions |
403 Forbidden |
Permissions in JWT tokens¶
Guards run in Rust without database access, so all permission data must be embedded in the JWT token itself.
How it works¶
- When you create a JWT token, you include the user's permissions in the token claims
- The Rust layer validates the token and extracts permissions from claims
- Guards check permissions against the extracted claims - no database queries
Creating tokens with permissions¶
The create_jwt_for_user() function automatically includes is_staff and is_superuser, but permissions must be passed explicitly via extra_claims:
from django_bolt.auth import create_jwt_for_user
# Basic token - includes is_staff, is_superuser, but NOT permissions
token = create_jwt_for_user(user, expires_in=3600)
# Token with permissions - required for HasPermission guards
token = create_jwt_for_user(
user,
expires_in=3600,
extra_claims={
"permissions": ["blog.add_article", "blog.change_article"]
}
)
Loading permissions from Django¶
To include a user's Django permissions in the token:
from django_bolt.auth import create_jwt_for_user
def create_token_with_permissions(user):
# Get all permissions for the user (from groups and direct assignments)
permissions = list(user.get_all_permissions())
return create_jwt_for_user(
user,
expires_in=3600,
extra_claims={"permissions": permissions}
)
Token claims reference¶
| Claim | Included By Default | Used By Guard |
|---|---|---|
is_staff |
Yes | IsStaff |
is_superuser |
Yes | IsAdminUser |
permissions |
No (use extra_claims) |
HasPermission, HasAnyPermission, HasAllPermissions |
Default guards¶
Set default guards for all endpoints:
api = BoltAPI(
default_auth=[JWTAuthentication()],
default_guards=[IsAuthenticated()]
)
# All endpoints require authentication by default
@api.get("/data")
async def get_data():
return {"protected": True}
# Override for public endpoints
@api.get("/health", guards=[AllowAny()])
async def health():
return {"status": "ok"}
Runtime permission checks¶
For complex permission logic, perform checks in your handler:
from django_bolt.exceptions import Forbidden
@api.delete(
"/articles/{article_id}",
auth=[JWTAuthentication()],
guards=[IsAuthenticated()]
)
async def delete_article(request, article_id: int):
article = await Article.objects.aget(id=article_id)
# Check if user owns the article or is admin
user = request.user
if article.author_id != user.id and not user.is_superuser:
raise Forbidden(detail="You can only delete your own articles")
await article.adelete()
return {"deleted": article_id}
Performance¶
Guards run in Rust before your Python handler is called. This means:
- Invalid requests are rejected without Python GIL overhead
- Authentication and authorization happen in a single pass
- Your handler only runs for authorized requests