Advanced Usage#
This page covers advanced usage patterns for axioms-drf-py authentication and permission classes.
Using Properties for Dynamic Permissions#
Properties provide a powerful way to dynamically determine permissions based on the HTTP method, request data, or other conditions.
Method-Level Permissions with Properties#
Use the @property decorator to return different permission requirements based on the request method:
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from axioms_drf.authentication import HasValidAccessToken
from axioms_drf.permissions import HasAccessTokenPermissions
class ArticleView(APIView):
"""Article endpoint with method-level permissions."""
authentication_classes = [HasValidAccessToken]
permission_classes = [HasAccessTokenPermissions]
@property
def access_token_permissions(self):
"""Return different permissions based on HTTP method."""
method_permissions = {
'GET': ['article:read'],
'POST': ['article:create'],
'PATCH': ['article:update'],
'PUT': ['article:update'],
'DELETE': ['article:delete']
}
return method_permissions.get(self.request.method, [])
def get(self, request):
"""Read article - requires article:read permission."""
return Response({'articles': []})
def post(self, request):
"""Create article - requires article:create permission."""
return Response({'id': 1}, status=status.HTTP_201_CREATED)
def patch(self, request):
"""Update article - requires article:update permission."""
return Response({'updated': True})
def delete(self, request):
"""Delete article - requires article:delete permission."""
return Response(status=status.HTTP_204_NO_CONTENT)
Dynamic Scopes Based on Request Data#
Use properties to determine required scopes based on request data:
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from axioms_drf.authentication import HasValidAccessToken
from axioms_drf.permissions import HasAccessTokenScopes
class DocumentView(APIView):
"""Document endpoint with dynamic scope requirements."""
authentication_classes = [HasValidAccessToken]
permission_classes = [HasAccessTokenScopes]
@property
def access_token_scopes(self):
"""Return scopes based on document sensitivity."""
# For GET requests, check query parameter
if self.request.method == 'GET':
sensitivity = self.request.query_params.get('sensitivity', 'normal')
if sensitivity == 'confidential':
return ['read:confidential']
return ['read:documents']
# For POST requests, check request body
elif self.request.method == 'POST':
sensitivity = self.request.data.get('sensitivity', 'normal')
if sensitivity == 'confidential':
return ['create:confidential']
return ['create:documents']
return ['read:documents']
def get(self, request):
"""Get documents - scope depends on sensitivity."""
sensitivity = request.query_params.get('sensitivity', 'normal')
return Response({
'documents': [],
'sensitivity': sensitivity
})
def post(self, request):
"""Create document - scope depends on sensitivity."""
return Response({'id': 1}, status=status.HTTP_201_CREATED)
Combining Multiple Permission Types#
Use properties to dynamically set multiple permission types (scopes, roles, permissions):
from rest_framework.views import APIView
from rest_framework.response import Response
from axioms_drf.authentication import HasValidAccessToken
from axioms_drf.permissions import (
HasAccessTokenScopes,
HasAccessTokenRoles,
HasAccessTokenPermissions
)
class AdminView(APIView):
"""Admin endpoint with dynamic multi-type permissions."""
authentication_classes = [HasValidAccessToken]
permission_classes = [
HasAccessTokenScopes,
HasAccessTokenRoles,
HasAccessTokenPermissions
]
@property
def access_token_scopes(self):
"""All methods require openid scope."""
return ['openid', 'profile']
@property
def access_token_roles(self):
"""Return roles based on HTTP method."""
if self.request.method in ['DELETE', 'PATCH']:
# Destructive operations require admin role
return ['admin']
# Read operations require editor or admin
return ['editor', 'admin']
@property
def access_token_permissions(self):
"""Return permissions based on HTTP method."""
method_permissions = {
'GET': ['admin:read'],
'POST': ['admin:create'],
'PATCH': ['admin:update'],
'DELETE': ['admin:delete']
}
return method_permissions.get(self.request.method, [])
def get(self, request):
"""Requires: openid+profile scopes, editor OR admin role, admin:read permission."""
return Response({'users': []})
def post(self, request):
"""Requires: openid+profile scopes, editor OR admin role, admin:create permission."""
return Response({'id': 1}, status=201)
def patch(self, request, pk):
"""Requires: openid+profile scopes, admin role, admin:update permission."""
return Response({'updated': True})
def delete(self, request, pk):
"""Requires: openid+profile scopes, admin role, admin:delete permission."""
return Response(status=204)
Using Properties with ViewSets#
ViewSets can also use properties for action-specific permissions:
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from axioms_drf.authentication import HasValidAccessToken
from axioms_drf.permissions import HasAccessTokenPermissions
class ArticleViewSet(viewsets.ModelViewSet):
"""ViewSet with action-specific permissions."""
authentication_classes = [HasValidAccessToken]
permission_classes = [HasAccessTokenPermissions]
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@property
def access_token_permissions(self):
"""Return permissions based on ViewSet action."""
action_permissions = {
'list': ['article:read'],
'retrieve': ['article:read'],
'create': ['article:create'],
'update': ['article:update'],
'partial_update': ['article:update'],
'destroy': ['article:delete'],
'publish': ['article:publish'], # Custom action
'archive': ['article:archive'], # Custom action
}
return action_permissions.get(self.action, [])
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""Publish article - requires article:publish permission."""
article = self.get_object()
article.published = True
article.save()
return Response({'status': 'published'})
@action(detail=True, methods=['post'])
def archive(self, request, pk=None):
"""Archive article - requires article:archive permission."""
article = self.get_object()
article.archived = True
article.save()
return Response({'status': 'archived'})
OR and AND Logic with Properties#
Use properties to dynamically set both OR and AND logic requirements:
from rest_framework.views import APIView
from rest_framework.response import Response
from axioms_drf.authentication import HasValidAccessToken
from axioms_drf.permissions import HasAccessTokenScopes, HasAccessTokenRoles
class ComplexPermissionView(APIView):
"""View with complex permission logic."""
authentication_classes = [HasValidAccessToken]
permission_classes = [HasAccessTokenScopes, HasAccessTokenRoles]
@property
def access_token_any_scopes(self):
"""User needs ANY of these scopes (OR logic)."""
return ['read:data', 'read:all']
@property
def access_token_all_scopes(self):
"""User needs ALL of these scopes (AND logic)."""
return ['openid', 'profile']
@property
def access_token_any_roles(self):
"""User needs ANY of these roles (OR logic)."""
if self.request.method == 'DELETE':
return ['admin'] # Only admin can delete
return ['user', 'editor', 'admin']
@property
def access_token_all_roles(self):
"""User needs ALL of these roles (AND logic)."""
# For sensitive operations, require both verified and active roles
if self.request.method in ['DELETE', 'PATCH']:
return ['verified', 'active']
return []
def get(self, request):
"""
Requires:
- (read:data OR read:all) AND (openid AND profile) scopes
- (user OR editor OR admin) roles
"""
return Response({'data': 'success'})
def delete(self, request):
"""
Requires:
- (read:data OR read:all) AND (openid AND profile) scopes
- admin role AND (verified AND active) roles
"""
return Response(status=204)
Object-Level Permissions with Properties#
Combine object-level permissions with properties for dynamic owner attribute:
from rest_framework import viewsets
from rest_framework.response import Response
from axioms_drf.authentication import HasValidAccessToken
from axioms_drf.permissions import IsSubOwner
class DynamicOwnerViewSet(viewsets.ModelViewSet):
"""ViewSet with dynamic owner attribute."""
authentication_classes = [HasValidAccessToken]
permission_classes = [IsSubOwner]
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@property
def owner_attribute(self):
"""
Dynamically determine which field contains the owner identifier.
Different models might use different field names.
"""
model = self.get_queryset().model
# Check model for common owner field names
if hasattr(model, 'author_sub'):
return 'author_sub'
elif hasattr(model, 'owner_sub'):
return 'owner_sub'
elif hasattr(model, 'user_id'):
return 'user_id'
# Default fallback
return 'user'
def perform_create(self, serializer):
"""Set the owner from token's sub claim."""
owner_field = self.owner_attribute
serializer.save(**{owner_field: self.request.user})
Best Practices#
Prefer Class-Based Views with Properties
Properties provide better control and are more maintainable than setting attributes on function views.
Use Type Hints
from typing import List @property def access_token_scopes(self) -> List[str]: return ['read:data']
Cache Property Results if Expensive
from functools import cached_property @cached_property def access_token_permissions(self) -> List[str]: # Expensive computation here return self._compute_permissions()
Document Permission Requirements
Always document what permissions each endpoint requires in docstrings.
Test All Permission Combinations
When using complex permission logic with properties, ensure thorough test coverage.
Common Pitfalls#
Pitfall 1: Setting Attributes on Function Views
# DON'T DO THIS - Not thread-safe
@api_view(['GET'])
@permission_classes([HasAccessTokenScopes])
def my_view(request):
my_view.access_token_scopes = ['read:data'] # Unsafe
return Response({'data': 'ok'})
Tip
Use class-based views with properties instead.
Pitfall 2: Accessing request.data in Property for GET Requests
# DON'T DO THIS
@property
def access_token_scopes(self):
# request.data is for POST/PUT/PATCH, not GET
if self.request.method == 'GET':
value = self.request.data.get('key') # Wrong
return ['read:data']
Tip
Use request.query_params for GET requests, request.data for POST/PUT/PATCH.
Pitfall 3: Returning Mutable Default
# DON'T DO THIS
@property
def access_token_scopes(self):
scopes = ['openid'] # Mutable list
if some_condition:
scopes.append('profile')
return scopes
Tip
Create a new list each time or use tuple for immutable defaults.
# DO THIS
@property
def access_token_scopes(self):
base_scopes = ['openid']
if some_condition:
return base_scopes + ['profile']
return base_scopes
See Also#
Examples - Basic usage examples
Issuer Configuration - Issuer configuration for different providers