A comprehensive, reusable Django authorization framework supporting RBAC and PBAC
Project description
tarxemo-django-authz
A comprehensive, production-ready Django authorization framework supporting both Role-Based Access Control (RBAC) and Policy-Based Access Control (PBAC). This library provides a complete solution for managing permissions, roles, and access control in your Django applications.
Table of Contents
- Features
- When to Use This Library
- Installation
- Quick Start
- Configuration
- Core Concepts
- Complete API Reference
- GraphQL API
- Usage Examples
- Django Admin
- Best Practices
- Testing
- Troubleshooting
- License
Features
✅ Role-Based Access Control (RBAC)
- Create roles that group multiple permissions
- Assign roles to users with full audit trails
- Flexible role management with active/inactive states
✅ Policy-Based Access Control (PBAC)
- Define custom policies for complex business rules
- Policies work alongside RBAC for fine-grained control
- Support for object-level and context-aware permissions
✅ Deny-First Security Model
- Explicit denies always override allows
- Secure by default - unauthorized access is denied
- Superuser bypass for administrative access
✅ Multiple Integration Points
- Function decorators for function-based views
- Mixins for class-based views
- Django REST Framework permission classes
- Template tags for conditional rendering
- Direct API calls for custom logic
✅ GraphQL Support
- Complete GraphQL API for managing permissions and roles
- Queries for listing and retrieving authorization data
- Mutations for creating, updating, and assigning permissions/roles
✅ Django Admin Integration
- Full admin interfaces for all models
- Search, filter, and bulk actions
- Read-only fields for security
✅ Production-Ready
- Comprehensive validation and error handling
- Optimized database queries with proper indexing
- Extensive test coverage
- Detailed logging for debugging
When to Use This Library
Use tarxemo-django-authz when you need:
- Fine-grained permissions beyond Django's built-in permission system
- Role-based access control to group permissions logically
- Custom authorization logic that depends on object state or context
- Separation of concerns between business logic and authorization
- GraphQL API for managing permissions from frontend applications
- Audit trails to track who assigned permissions and when
- Multi-tenant applications with complex permission requirements
RBAC vs PBAC
RBAC (Role-Based Access Control):
- Users are assigned roles (e.g., "Editor", "Manager")
- Roles contain collections of permissions
- Best for: Organizational hierarchies, job functions
PBAC (Policy-Based Access Control):
- Custom Python code evaluates permissions
- Can check object ownership, business rules, time-based access, etc.
- Best for: Resource ownership, dynamic rules, complex conditions
This library lets you use both together - RBAC for general permissions, PBAC for special cases.
Installation
From PyPI (Recommended)
pip install tarxemo-django-authz
From GitHub (Development)
pip install git+https://github.com/tarxemo/tarxemo-django-authz.git
Dependencies
This library requires:
- Django >= 3.2
- graphene-django >= 3.0 (for GraphQL support)
- tarxemo-django-graphene-utils >= 0.1.2 (for GraphQL utilities)
Optional:
- djangorestframework >= 3.12 (for DRF integration)
Install with DRF support:
pip install tarxemo-django-authz[drf]
Quick Start
Follow these steps to get started with tarxemo-django-authz in just a few minutes.
Step 1: Add to INSTALLED_APPS
Add authz and its dependency to your Django settings.py:
INSTALLED_APPS = [
# ... your other apps
'tarxemo_django_graphene_utils', # Required dependency
'authz',
]
Step 2: Run Migrations
Create the necessary database tables:
python manage.py migrate authz
This creates four tables:
authz_permission- Stores permission definitionsauthz_role- Stores role definitionsauthz_userrole- Links users to rolesauthz_userpermission- Stores explicit user permission overrides
Step 3: Create Your First Permission
from authz.models import Permission
# Create a permission for creating articles
Permission.objects.create(
code="articles.create",
description="Allows user to create new articles"
)
# Create more permissions
Permission.objects.create(
code="articles.edit",
description="Allows user to edit existing articles"
)
Permission.objects.create(
code="articles.delete",
description="Allows user to delete articles"
)
Permission Naming Convention: Use the format namespace.action (e.g., articles.create, users.view, reports.export)
Step 4: Create a Role
from authz.models import Role
# Create an "Author" role
author_role = Role.objects.create(
name="Author",
description="Can create and edit their own articles"
)
# Add permissions to the role
author_role.add_permission("articles.create")
author_role.add_permission("articles.edit")
Step 5: Assign Role to a User
from authz.services import assign_role
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.get(username="john")
# Assign the Author role
assign_role(user, "Author", created_by=request.user)
Step 6: Check Permissions
Now you can check if a user has permission:
from authz.engine import authorize
# Check if user can create articles
if authorize(user, "articles.create"):
# User has permission - allow action
article = Article.objects.create(...)
else:
# User doesn't have permission - deny action
return HttpResponseForbidden("You don't have permission to create articles")
That's it! You now have a working authorization system. Continue reading for advanced features and complete API documentation.
Configuration
Required Settings
Add authz to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
'tarxemo_django_graphene_utils',
'authz',
]
Optional Settings
Currently, authz works out of the box with no additional configuration. All settings use Django defaults.
Authentication Model Compatibility
The library works with any Django authentication model:
- Django's default
Usermodel - Custom user models (via
AUTH_USER_MODEL) - Third-party authentication packages
The library uses settings.AUTH_USER_MODEL to reference your user model, so it automatically adapts to your setup.
Core Concepts
Models
Permission
Represents a single permission in your system.
Fields:
code(CharField, unique) - Permission identifier (e.g., "articles.create")description(TextField) - Human-readable descriptionis_active(BooleanField) - Whether permission is currently activecreated_at,updated_at- Timestamps
Methods:
__str__()- Returns the permission code
Example:
permission = Permission.objects.create(
code="courses.enroll",
description="Allows students to enroll in courses"
)
Role
A collection of permissions that can be assigned to users.
Fields:
name(CharField, unique) - Role name (e.g., "Student", "Instructor")description(TextField) - Role descriptionpermissions(ManyToManyField) - Permissions included in this roleis_active(BooleanField) - Whether role is currently activecreated_at,updated_at- Timestamps
Methods:
get_permission_codes()- Returns QuerySet of permission codesadd_permission(permission_code)- Add a permission to this roleremove_permission(permission_code)- Remove a permission from this role
Example:
role = Role.objects.create(
name="Student",
description="Regular student with basic access"
)
role.add_permission("courses.view")
role.add_permission("courses.enroll")
UserRole
Links users to roles with audit information.
Fields:
user(ForeignKey) - The userrole(ForeignKey) - The rolecreated_at(DateTimeField) - When role was assignedcreated_by(ForeignKey, nullable) - Who assigned the role
Constraints:
- Unique together: (user, role) - A user can't have the same role twice
Example:
from authz.services import assign_role
assign_role(user, "Student", created_by=admin_user)
UserPermission
Direct permission overrides for individual users.
Fields:
user(ForeignKey) - The userpermission(ForeignKey) - The permissionallow(BooleanField) - True = grant, False = denycreated_at(DateTimeField) - When override was createdcreated_by(ForeignKey, nullable) - Who created the override
Constraints:
- Unique together: (user, permission) - One override per user per permission
Example:
from authz.services import grant_permission, deny_permission
# Explicitly grant a permission
grant_permission(user, "articles.delete", created_by=admin_user)
# Explicitly deny a permission (overrides role permissions)
deny_permission(user, "courses.grade", created_by=admin_user)
Authorization Flow
When you call authorize(user, permission_code), the system evaluates permissions in this order:
- ❌ Anonymous Users → DENY (unless policies explicitly allow)
- ✅ Superusers → ALLOW (bypass all checks)
- ❌ Explicit User Deny → DENY (UserPermission with allow=False)
- ✅ Explicit User Allow → ALLOW (UserPermission with allow=True)
- ✅ Role-Based Permission → ALLOW (if user has role with permission)
- ✅ Model Defaults → ALLOW/DENY (if object implements default permission logic)
- ❌ Default → DENY (secure by default)
At each step, policies are evaluated. If any policy denies, the request is denied regardless of RBAC grants.
Policies
Policies are Python classes that implement custom authorization logic. They work alongside RBAC to add additional constraints.
Key Points:
- Policies are evaluated after RBAC checks
- Policies can only deny what RBAC grants (they can't grant new permissions)
- Multiple policies can be registered for the same permission
- If any policy denies, the request is denied
Example Policy:
from authz.policies import BasePolicy, register_policy
@register_policy
class OwnerOnlyPolicy(BasePolicy):
permission_code = "articles.edit"
def allows(self, user, obj=None, context=None):
"""Only allow if user owns the article"""
if obj and hasattr(obj, 'author'):
return obj.author == user
return False
def get_denial_reason(self, user, obj=None, context=None):
return "You can only edit your own articles"
Now, even if a user has the articles.edit permission via a role, they can only edit articles they own.
Complete API Reference
Engine Functions
These are the core functions for checking permissions.
authorize(user, permission_code, obj=None, context=None)
Main authorization function. Checks if a user has permission to perform an action.
Parameters:
user- User instance to checkpermission_code(str) - Permission code (e.g., "articles.create")obj(optional) - Object being accessed (for object-level permissions)context(dict, optional) - Additional context for policy evaluation
Returns: bool - True if authorized, False otherwise
Example:
from authz.engine import authorize
# Simple permission check
if authorize(user, "articles.create"):
article = Article.objects.create(...)
# Object-level permission check
article = Article.objects.get(pk=1)
if authorize(user, "articles.edit", obj=article):
article.title = "New Title"
article.save()
# With context
if authorize(user, "reports.export", context={"format": "pdf"}):
generate_pdf_report()
has_permission(user, permission_code, obj=None, context=None)
Alias for authorize() with a more Django-like name.
Example:
from authz.engine import has_permission
if has_permission(user, "courses.enroll"):
enrollment = Enrollment.objects.create(...)
check_permission(user, permission_code, obj=None, context=None)
Another alias for authorize().
get_user_permission_codes(user, include_roles=True)
Get all permission codes for a user.
Parameters:
user- User instanceinclude_roles(bool) - Whether to include role-based permissions (default: True)
Returns: set - Set of permission code strings
Example:
from authz.engine import get_user_permission_codes
codes = get_user_permission_codes(user)
print(codes)
# {'articles.create', 'articles.edit', 'courses.view', ...}
# Only explicit permissions (no roles)
explicit_codes = get_user_permission_codes(user, include_roles=False)
Service Functions
High-level functions for managing roles and permissions.
Role Management
assign_role(user, role_name, created_by=None)
Assign a role to a user.
Parameters:
user- User instancerole_name(str) - Name of the rolecreated_by(User, optional) - User who is assigning the role (for audit trail)
Returns: UserRole instance
Raises: Role.DoesNotExist if role doesn't exist
Example:
from authz.services import assign_role
user_role = assign_role(user, "Editor", created_by=request.user)
revoke_role(user, role_name)
Remove a role from a user.
Parameters:
user- User instancerole_name(str) - Name of the role
Returns: bool - True if role was revoked, False if user didn't have the role
Example:
from authz.services import revoke_role
success = revoke_role(user, "Editor")
if success:
print("Role revoked")
get_user_roles(user)
Get all roles assigned to a user.
Parameters:
user- User instance
Returns: List[Role] - List of Role objects
Example:
from authz.services import get_user_roles
roles = get_user_roles(user)
for role in roles:
print(f"User has role: {role.name}")
get_user_role_names(user)
Get role names as strings.
Parameters:
user- User instance
Returns: List[str] - List of role names
Example:
from authz.services import get_user_role_names
role_names = get_user_role_names(user)
print(role_names) # ['Student', 'TA']
user_has_role(user, role_name)
Check if user has a specific role.
Parameters:
user- User instancerole_name(str) - Role name to check
Returns: bool
Example:
from authz.services import user_has_role
if user_has_role(user, "Admin"):
# Show admin panel
pass
bulk_assign_roles(users, role_names, created_by=None)
Assign multiple roles to multiple users.
Parameters:
users(List[User]) - List of usersrole_names(List[str]) - List of role namescreated_by(User, optional) - User performing the assignment
Returns: int - Number of role assignments created
Raises: Role.DoesNotExist if any role doesn't exist
Example:
from authz.services import bulk_assign_roles
users = User.objects.filter(department="Engineering")
count = bulk_assign_roles(users, ["Developer", "Tester"], created_by=admin)
print(f"Created {count} role assignments")
Permission Management
grant_permission(user, permission_code, created_by=None)
Grant a permission directly to a user (explicit allow).
Parameters:
user- User instancepermission_code(str) - Permission codecreated_by(User, optional) - User granting the permission
Returns: UserPermission instance
Raises: Permission.DoesNotExist if permission doesn't exist
Example:
from authz.services import grant_permission
# Grant special permission to one user
user_perm = grant_permission(user, "reports.export_all", created_by=admin)
deny_permission(user, permission_code, created_by=None)
Explicitly deny a permission for a user (overrides role permissions).
Parameters:
user- User instancepermission_code(str) - Permission codecreated_by(User, optional) - User creating the denial
Returns: UserPermission instance
Raises: Permission.DoesNotExist if permission doesn't exist
Example:
from authz.services import deny_permission
# Deny a specific permission even if user has it via role
user_perm = deny_permission(user, "users.delete", created_by=admin)
revoke_user_permission(user, permission_code)
Remove an explicit permission override (allow or deny).
Parameters:
user- User instancepermission_code(str) - Permission code
Returns: bool - True if override was removed, False if no override existed
Example:
from authz.services import revoke_user_permission
success = revoke_user_permission(user, "reports.export_all")
get_user_permissions(user, include_roles=True)
Get all permissions for a user with their sources.
Parameters:
user- User instanceinclude_roles(bool) - Whether to include role-based permissions
Returns: List[dict] - List of permission dictionaries
Example:
from authz.services import get_user_permissions
permissions = get_user_permissions(user)
for perm in permissions:
print(f"{perm['code']} from {perm['source']}")
# Output: "articles.create from role (Author)"
# "reports.export from explicit"
get_permission_matrix(user)
Get a comprehensive permission matrix for a user.
Parameters:
user- User instance
Returns: dict - Permission matrix with roles and permissions
Example:
from authz.services import get_permission_matrix
matrix = get_permission_matrix(user)
print(matrix)
# {
# 'explicit_allows': ['reports.export'],
# 'explicit_denies': [],
# 'roles': {
# 'Author': ['articles.create', 'articles.edit'],
# 'Reviewer': ['articles.review']
# },
# 'all_permissions': ['articles.create', 'articles.edit', 'articles.review', 'reports.export']
# }
Utility Functions
get_users_with_permission(permission_code)
Find all users who have a specific permission (via roles or explicit grants).
Parameters:
permission_code(str) - Permission code
Returns: List[User] - List of users
Example:
from authz.services import get_users_with_permission
users = get_users_with_permission("articles.publish")
for user in users:
notify_user(user, "New article pending review")
get_users_with_role(role_name)
Find all users who have a specific role.
Parameters:
role_name(str) - Role name
Returns: List[User] - List of users
Example:
from authz.services import get_users_with_role
admins = get_users_with_role("Admin")
Decorators
@require_permission(permission_code)
Decorator for function-based views that requires a permission.
Parameters:
permission_code(str) - Required permission code
Behavior:
- If user has permission: View executes normally
- If user lacks permission: Returns HTTP 403 Forbidden
Example:
from authz.decorators import require_permission
from django.http import JsonResponse
@require_permission("articles.create")
def create_article(request):
# Only users with "articles.create" permission can access this
article = Article.objects.create(
title=request.POST['title'],
author=request.user
)
return JsonResponse({"id": article.id})
With object-level permissions:
from authz.decorators import require_permission
@require_permission("articles.edit")
def edit_article(request, article_id):
article = Article.objects.get(pk=article_id)
# Additional object-level check
from authz.engine import authorize
if not authorize(request.user, "articles.edit", obj=article):
return HttpResponseForbidden("You can only edit your own articles")
article.title = request.POST['title']
article.save()
return JsonResponse({"success": True})
Mixins
PermissionRequiredMixin
Mixin for class-based views that requires a permission.
Attributes:
permission_required(str) - Required permission codepermission_denied_message(str, optional) - Custom error message
Behavior:
- Checks permission before dispatching the view
- Returns HTTP 403 if permission is denied
Example:
from authz.mixins import PermissionRequiredMixin
from django.views.generic import CreateView
class ArticleCreateView(PermissionRequiredMixin, CreateView):
model = Article
permission_required = "articles.create"
permission_denied_message = "You need Author role to create articles"
template_name = "articles/create.html"
fields = ['title', 'content']
With multiple permissions:
class ArticlePublishView(PermissionRequiredMixin, UpdateView):
model = Article
permission_required = "articles.publish" # Only one permission supported
# For multiple permissions, override has_permission method
Django REST Framework Integration
AuthzPermission
DRF permission class for API views.
Usage:
from rest_framework import viewsets
from authz.drf_permissions import AuthzPermission
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [AuthzPermission]
# Define permission for each action
permission_code = "articles.view" # Default for all actions
def get_permission_code(self):
"""Override to use different permissions per action"""
if self.action == 'create':
return "articles.create"
elif self.action in ['update', 'partial_update']:
return "articles.edit"
elif self.action == 'destroy':
return "articles.delete"
return "articles.view"
Object-level permissions:
class ArticleViewSet(viewsets.ModelViewSet):
permission_classes = [AuthzPermission]
def get_permission_code(self):
if self.action in ['update', 'partial_update']:
return "articles.edit"
return "articles.view"
def check_object_permissions(self, request, obj):
"""DRF calls this for object-level checks"""
super().check_object_permissions(request, obj)
# Additional authz check with object
from authz.engine import authorize
if not authorize(request.user, self.get_permission_code(), obj=obj):
self.permission_denied(request, message="You can only edit your own articles")
Template Tags
{% load authz_tags %}
Load the authz template tags.
{% if_has_permission 'permission_code' object %}
Conditionally render content based on permission.
Parameters:
permission_code(str) - Permission to checkobject(optional) - Object for object-level permission check
Example:
{% load authz_tags %}
<div class="article">
<h1>{{ article.title }}</h1>
<p>{{ article.content }}</p>
{% if_has_permission 'articles.edit' article %}
<a href="{% url 'edit_article' article.pk %}" class="btn">Edit</a>
{% endif_has_permission %}
{% if_has_permission 'articles.delete' article %}
<a href="{% url 'delete_article' article.pk %}" class="btn btn-danger">Delete</a>
{% endif_has_permission %}
</div>
Without object:
{% if_has_permission 'articles.create' %}
<a href="{% url 'create_article' %}" class="btn btn-primary">Create New Article</a>
{% endif_has_permission %}
GraphQL API
The library provides a complete GraphQL API for managing permissions and roles.
Setup
Include the authz schema in your main GraphQL schema:
import graphene
from authz.queries import AuthzQuery
from authz.mutations import AuthzMutation
class Query(AuthzQuery, graphene.ObjectType):
pass
class Mutation(AuthzMutation, graphene.ObjectType):
pass
schema = graphene.Schema(query=Query, mutation=Mutation)
Queries
permissions(search, page, page_size)
List all permissions with optional search and pagination.
Arguments:
search(String, optional) - Search in code and descriptionpage(Int, optional) - Page number (default: 1)page_size(Int, optional) - Items per page (default: 20)
Returns: PermissionListDTO
Example:
query {
permissions(search: "articles", page: 1, pageSize: 10) {
response {
status
message
}
data {
id
code
description
isActive
}
}
}
roles(search, page, page_size)
List all roles with optional search and pagination.
Arguments:
search(String, optional) - Search in name and descriptionpage(Int, optional) - Page numberpage_size(Int, optional) - Items per page
Returns: RoleListDTO
Example:
query {
roles(search: "author", page: 1, pageSize: 10) {
response {
status
message
}
data {
id
name
description
permissions {
code
description
}
}
}
}
role(id)
Get a single role by ID.
Arguments:
id(UUID, required) - Role ID
Returns: RoleSingleDTO
Example:
query {
role(id: "123e4567-e89b-12d3-a456-426614174000") {
response {
status
message
}
data {
role {
id
name
description
permissions {
code
description
}
}
}
}
}
userAuthorizationDetails(userId)
Get complete authorization details for a user.
Arguments:
userId(ID, required) - User ID
Returns: UserAuthorizationDetailsDTO
Example:
query {
userAuthorizationDetails(userId: "123") {
response {
status
message
}
data {
roles {
name
permissions {
code
}
}
explicitPermissions {
permission {
code
}
allow
}
allPermissionCodes
}
}
}
Mutations
createPermission(input)
Create a new permission.
Input:
code(String, required) - Permission codedescription(String, optional) - Description
Example:
mutation {
createPermission(input: {
code: "articles.publish"
description: "Allows publishing articles"
}) {
response {
status
message
}
data {
id
code
description
}
}
}
updatePermission(input)
Update an existing permission.
Input:
id(UUID, required) - Permission IDdescription(String, optional) - New descriptionisActive(Boolean, optional) - Active status
Example:
mutation {
updatePermission(input: {
id: "123e4567-e89b-12d3-a456-426614174000"
description: "Updated description"
isActive: true
}) {
response {
status
message
}
data {
id
code
description
}
}
}
deletePermission(id)
Delete a permission.
Arguments:
id(ID, required) - Permission ID
Example:
mutation {
deletePermission(id: "123e4567-e89b-12d3-a456-426614174000") {
response {
status
message
}
}
}
createRole(input)
Create a new role.
Input:
name(String, required) - Role namedescription(String, optional) - DescriptionpermissionCodes(List[String], optional) - Permission codes to add
Example:
mutation {
createRole(input: {
name: "Content Manager"
description: "Manages all content"
permissionCodes: ["articles.create", "articles.edit", "articles.publish"]
}) {
response {
status
message
}
data {
id
name
permissions {
code
}
}
}
}
updateRole(input)
Update an existing role.
Input:
id(UUID, required) - Role IDname(String, optional) - New namedescription(String, optional) - New descriptionpermissionCodes(List[String], optional) - New permission codes (replaces existing)isActive(Boolean, optional) - Active status
Example:
mutation {
updateRole(input: {
id: "123e4567-e89b-12d3-a456-426614174000"
name: "Senior Editor"
permissionCodes: ["articles.create", "articles.edit", "articles.publish", "articles.delete"]
}) {
response {
status
message
}
}
}
deleteRole(id)
Delete a role.
Arguments:
id(ID, required) - Role ID
Example:
mutation {
deleteRole(id: "123e4567-e89b-12d3-a456-426614174000") {
response {
status
message
}
}
}
assignRole(input)
Assign a role to a user.
Input:
userId(ID, required) - User IDroleName(String, required) - Role name
Example:
mutation {
assignRole(input: {
userId: "456"
roleName: "Author"
}) {
response {
status
message
}
}
}
revokeRole(input)
Revoke a role from a user.
Input:
userId(ID, required) - User IDroleName(String, required) - Role name
Example:
mutation {
revokeRole(input: {
userId: "456"
roleName: "Author"
}) {
response {
status
message
}
}
}
grantPermission(input)
Grant an explicit permission to a user.
Input:
userId(ID, required) - User IDpermissionCode(String, required) - Permission code
Example:
mutation {
grantPermission(input: {
userId: "456"
permissionCode: "reports.export_all"
}) {
response {
status
message
}
}
}
denyPermission(input)
Explicitly deny a permission for a user.
Input:
userId(ID, required) - User IDpermissionCode(String, required) - Permission code
Example:
mutation {
denyPermission(input: {
userId: "456"
permissionCode: "users.delete"
}) {
response {
status
message
}
}
}
revokeUserPermission(input)
Remove an explicit permission override.
Input:
userId(ID, required) - User IDpermissionCode(String, required) - Permission code
Example:
mutation {
revokeUserPermission(input: {
userId: "456"
permissionCode: "reports.export_all"
}) {
response {
status
message
}
}
}
Usage Examples
Example 1: Basic Permission Checking
from authz.engine import authorize
from django.http import HttpResponseForbidden
def delete_article(request, article_id):
article = Article.objects.get(pk=article_id)
# Check permission
if not authorize(request.user, "articles.delete"):
return HttpResponseForbidden("You don't have permission to delete articles")
article.delete()
return JsonResponse({"success": True})
Example 2: Role-Based Dashboard Access
from authz.services import user_has_role
from django.shortcuts import render, redirect
def dashboard(request):
user = request.user
if user_has_role(user, "Admin"):
return render(request, "admin_dashboard.html")
elif user_has_role(user, "Manager"):
return render(request, "manager_dashboard.html")
elif user_has_role(user, "Employee"):
return render(request, "employee_dashboard.html")
else:
return redirect("access_denied")
Example 3: Resource Ownership Policy
from authz.policies import BasePolicy, register_policy
@register_policy
class ArticleOwnerPolicy(BasePolicy):
permission_code = "articles.edit"
def allows(self, user, obj=None, context=None):
"""Only allow editing own articles"""
if not obj:
return True # Allow if no specific object (e.g., list view)
if hasattr(obj, 'author'):
return obj.author == user
return False
def get_denial_reason(self, user, obj=None, context=None):
return "You can only edit articles you authored"
# Now use it in a view
from authz.engine import authorize
def edit_article(request, article_id):
article = Article.objects.get(pk=article_id)
# This will check both RBAC and the ownership policy
if not authorize(request.user, "articles.edit", obj=article):
return HttpResponseForbidden("You can only edit your own articles")
# Process edit
article.title = request.POST['title']
article.save()
return JsonResponse({"success": True})
Example 4: Feature Flags
# Create feature permissions
from authz.models import Permission, Role
# Create premium features
Permission.objects.create(
code="features.advanced_analytics",
description="Access to advanced analytics dashboard"
)
Permission.objects.create(
code="features.export_data",
description="Export data to CSV/Excel"
)
# Create premium role
premium_role = Role.objects.create(
name="Premium User",
description="Users with premium subscription"
)
premium_role.add_permission("features.advanced_analytics")
premium_role.add_permission("features.export_data")
# In your view
from authz.engine import authorize
def analytics_dashboard(request):
if not authorize(request.user, "features.advanced_analytics"):
return render(request, "upgrade_to_premium.html")
# Show advanced analytics
return render(request, "analytics.html")
Example 5: Multi-Tenant Permissions
from authz.policies import BasePolicy, register_policy
@register_policy
class TenantAccessPolicy(BasePolicy):
permission_code = "documents.view"
def allows(self, user, obj=None, context=None):
"""Only allow access to documents in user's tenant"""
if not obj:
return True
if hasattr(obj, 'tenant_id') and hasattr(user, 'tenant_id'):
return obj.tenant_id == user.tenant_id
return False
def get_denial_reason(self, user, obj=None, context=None):
return "You can only access documents in your organization"
Example 6: Time-Based Access
from authz.policies import BasePolicy, register_policy
from django.utils import timezone
@register_policy
class BusinessHoursPolicy(BasePolicy):
permission_code = "reports.generate"
def allows(self, user, obj=None, context=None):
"""Only allow report generation during business hours"""
now = timezone.now()
hour = now.hour
# Business hours: 8 AM to 6 PM
if 8 <= hour < 18:
return True
# Admins can generate reports anytime
return user.is_superuser
def get_denial_reason(self, user, obj=None, context=None):
return "Reports can only be generated during business hours (8 AM - 6 PM)"
Example 7: Conditional Rendering in Templates
{% load authz_tags %}
<div class="article-list">
{% for article in articles %}
<div class="article">
<h2>{{ article.title }}</h2>
<p>{{ article.excerpt }}</p>
<div class="actions">
<a href="{% url 'view_article' article.pk %}">View</a>
{% if_has_permission 'articles.edit' article %}
<a href="{% url 'edit_article' article.pk %}">Edit</a>
{% endif_has_permission %}
{% if_has_permission 'articles.delete' article %}
<a href="{% url 'delete_article' article.pk %}" class="danger">Delete</a>
{% endif_has_permission %}
</div>
</div>
{% endfor %}
{% if_has_permission 'articles.create' %}
<a href="{% url 'create_article' %}" class="btn btn-primary">Create New Article</a>
{% endif_has_permission %}
</div>
Example 8: DRF API with Per-Action Permissions
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from authz.drf_permissions import AuthzPermission
from authz.engine import authorize
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [AuthzPermission]
def get_permission_code(self):
"""Different permissions for different actions"""
action_permissions = {
'list': 'articles.view',
'retrieve': 'articles.view',
'create': 'articles.create',
'update': 'articles.edit',
'partial_update': 'articles.edit',
'destroy': 'articles.delete',
'publish': 'articles.publish',
}
return action_permissions.get(self.action, 'articles.view')
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""Custom action to publish an article"""
article = self.get_object()
# Permission already checked by AuthzPermission
article.status = 'published'
article.published_at = timezone.now()
article.save()
return Response({'status': 'published'})
def perform_update(self, serializer):
"""Additional object-level check"""
article = self.get_object()
# Check if user can edit this specific article
if not authorize(self.request.user, "articles.edit", obj=article):
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("You can only edit your own articles")
serializer.save()
Django Admin
The library provides full Django admin integration for managing permissions and roles.
Accessing the Admin
Navigate to /admin/authz/ to access the authorization admin.
Available Admin Interfaces
Permission Admin (/admin/authz/permission/)
Features:
- List all permissions with search and filtering
- Search by code or description
- Filter by active status
- Create, edit, and delete permissions
- Bulk actions
Fields:
- Code (editable)
- Description (editable)
- Is Active (editable)
- Created At (read-only)
- Updated At (read-only)
Role Admin (/admin/authz/role/)
Features:
- List all roles with search and filtering
- Search by name or description
- Filter by active status
- Manage role permissions via inline interface
- Create, edit, and delete roles
Fields:
- Name (editable)
- Description (editable)
- Permissions (many-to-many widget)
- Is Active (editable)
- Created At (read-only)
- Updated At (read-only)
UserRole Admin (/admin/authz/userrole/)
Features:
- View all user-role assignments
- Search by user or role
- Filter by role or creation date
- See who assigned each role and when
Fields:
- User (editable)
- Role (editable)
- Created At (read-only)
- Created By (read-only)
UserPermission Admin (/admin/authz/userpermission/)
Features:
- View all explicit permission overrides
- Search by user or permission
- Filter by allow/deny status
- See who created each override
Fields:
- User (editable)
- Permission (editable)
- Allow (editable - True = grant, False = deny)
- Created At (read-only)
- Created By (read-only)
Admin Best Practices
- Use Search: With many permissions/roles, use the search box to find what you need
- Bulk Actions: Select multiple items and use bulk actions for efficiency
- Audit Trail: Always check "Created By" and "Created At" for audit purposes
- Read-Only Fields: Timestamps and audit fields are read-only for data integrity
Best Practices
1. Permission Naming Conventions
Use the namespace.action format for all permissions:
Good:
articles.createarticles.editarticles.deletearticles.publishusers.viewusers.editreports.export
Bad:
create_article(no namespace)articles(no action)ArticleCreate(wrong case)
Benefits:
- Easy to understand and search
- Groups related permissions
- Consistent across the application
2. Role Design Patterns
Create roles based on job functions or user types:
Examples:
# Content roles
author_role = Role.objects.create(name="Author")
author_role.add_permission("articles.create")
author_role.add_permission("articles.edit") # Own articles only via policy
editor_role = Role.objects.create(name="Editor")
editor_role.add_permission("articles.create")
editor_role.add_permission("articles.edit")
editor_role.add_permission("articles.publish")
# User management roles
user_manager_role = Role.objects.create(name="User Manager")
user_manager_role.add_permission("users.view")
user_manager_role.add_permission("users.edit")
user_manager_role.add_permission("users.create")
# Admin role
admin_role = Role.objects.create(name="Admin")
# Add all permissions
3. Use Policies for Complex Rules
Don't try to model everything with RBAC. Use policies for:
- Resource ownership
- Time-based access
- Quota limits
- Business rules
- Conditional logic
Example:
@register_policy
class ArticleEditPolicy(BasePolicy):
permission_code = "articles.edit"
def allows(self, user, obj=None, context=None):
# Authors can only edit their own articles
# Editors can edit any article
if user_has_role(user, "Editor"):
return True
if obj and hasattr(obj, 'author'):
return obj.author == user
return False
4. Explicit Denies - Use Sparingly
Explicit denies override all role permissions. Only use them when:
- Temporarily suspending a user's specific permission
- Overriding a role permission for a specific user
- Implementing exceptions to general rules
Example:
# User is an Editor but we want to prevent them from deleting
from authz.services import deny_permission
deny_permission(user, "articles.delete", created_by=admin)
5. Always Pass created_by
For audit trails, always pass created_by when assigning roles or permissions:
from authz.services import assign_role, grant_permission
# Good
assign_role(user, "Author", created_by=request.user)
grant_permission(user, "reports.export", created_by=request.user)
# Bad (no audit trail)
assign_role(user, "Author")
6. Check Permissions on Both Frontend and Backend
Frontend (UI):
{% if_has_permission 'articles.delete' article %}
<button>Delete</button>
{% endif_has_permission %}
Backend (Security):
@require_permission("articles.delete")
def delete_article(request, article_id):
# Actual deletion logic
pass
Never rely on frontend checks alone - always enforce on the backend.
7. Performance Optimization
Use select_related and prefetch_related:
# When querying users with roles
users = User.objects.prefetch_related('user_roles__role__permissions').all()
# When checking multiple permissions
from authz.engine import get_user_permission_codes
codes = get_user_permission_codes(user) # Cached result
Cache permission checks for the request:
# In middleware or view
def my_view(request):
# Cache user permissions for this request
request.user._permission_cache = get_user_permission_codes(request.user)
# Now multiple authorize() calls will be faster
8. Testing Authorization
Always test both positive and negative cases:
from django.test import TestCase
from authz.engine import authorize
from authz.services import assign_role
class AuthorizationTestCase(TestCase):
def test_author_can_create_articles(self):
"""Test that authors can create articles"""
assign_role(self.user, "Author")
self.assertTrue(authorize(self.user, "articles.create"))
def test_guest_cannot_create_articles(self):
"""Test that guests cannot create articles"""
self.assertFalse(authorize(self.user, "articles.create"))
def test_author_can_only_edit_own_articles(self):
"""Test ownership policy"""
assign_role(self.user, "Author")
# Own article
own_article = Article.objects.create(author=self.user)
self.assertTrue(authorize(self.user, "articles.edit", obj=own_article))
# Someone else's article
other_article = Article.objects.create(author=self.other_user)
self.assertFalse(authorize(self.user, "articles.edit", obj=other_article))
Testing
Running the Test Suite
The library includes comprehensive tests. Run them with:
python manage.py test authz
Expected output:
Creating test database...
...........................
----------------------------------------------------------------------
Ran 28 tests in 2.345s
OK
Writing Tests for Your Authorization Logic
Example test file:
from django.test import TestCase
from django.contrib.auth import get_user_model
from authz.models import Permission, Role
from authz.services import assign_role, grant_permission, deny_permission
from authz.engine import authorize
User = get_user_model()
class ArticleAuthorizationTest(TestCase):
def setUp(self):
"""Set up test data"""
# Create users
self.author = User.objects.create_user(username="author")
self.editor = User.objects.create_user(username="editor")
self.guest = User.objects.create_user(username="guest")
# Create permissions
Permission.objects.create(code="articles.create")
Permission.objects.create(code="articles.edit")
Permission.objects.create(code="articles.delete")
Permission.objects.create(code="articles.publish")
# Create roles
author_role = Role.objects.create(name="Author")
author_role.add_permission("articles.create")
author_role.add_permission("articles.edit")
editor_role = Role.objects.create(name="Editor")
editor_role.add_permission("articles.create")
editor_role.add_permission("articles.edit")
editor_role.add_permission("articles.publish")
# Assign roles
assign_role(self.author, "Author")
assign_role(self.editor, "Editor")
def test_author_can_create(self):
"""Authors can create articles"""
self.assertTrue(authorize(self.author, "articles.create"))
def test_author_cannot_publish(self):
"""Authors cannot publish articles"""
self.assertFalse(authorize(self.author, "articles.publish"))
def test_editor_can_publish(self):
"""Editors can publish articles"""
self.assertTrue(authorize(self.editor, "articles.publish"))
def test_guest_has_no_permissions(self):
"""Guests have no permissions"""
self.assertFalse(authorize(self.guest, "articles.create"))
self.assertFalse(authorize(self.guest, "articles.edit"))
self.assertFalse(authorize(self.guest, "articles.publish"))
def test_explicit_deny_overrides_role(self):
"""Explicit deny overrides role permissions"""
# Editor normally can publish
self.assertTrue(authorize(self.editor, "articles.publish"))
# Deny the permission
deny_permission(self.editor, "articles.publish")
# Now they can't
self.assertFalse(authorize(self.editor, "articles.publish"))
def test_explicit_grant(self):
"""Explicit grant gives permission"""
# Guest normally can't create
self.assertFalse(authorize(self.guest, "articles.create"))
# Grant the permission
grant_permission(self.guest, "articles.create")
# Now they can
self.assertTrue(authorize(self.guest, "articles.create"))
Troubleshooting
Common Issues and Solutions
Issue: "Permission denied" even though user has the role
Possible causes:
- Role is inactive (
is_active=False) - Permission is inactive
- User has an explicit deny
- A policy is denying access
Solution:
from authz.services import get_permission_matrix
# Check user's complete permission matrix
matrix = get_permission_matrix(user)
print(matrix)
# Check for explicit denies
if permission_code in matrix['explicit_denies']:
print("User has explicit deny for this permission")
# Check if role is active
from authz.models import Role
role = Role.objects.get(name="Author")
print(f"Role active: {role.is_active}")
# Check if permission is active
from authz.models import Permission
perm = Permission.objects.get(code="articles.create")
print(f"Permission active: {perm.is_active}")
Issue: Migrations fail with "relation already exists"
Cause: Database tables already exist from a previous installation
Solution:
# Option 1: Fake the initial migration
python manage.py migrate authz --fake-initial
# Option 2: Drop tables and re-migrate (CAUTION: loses data)
python manage.py migrate authz zero
python manage.py migrate authz
Issue: "Role.DoesNotExist" when assigning role
Cause: Role hasn't been created yet
Solution:
from authz.models import Role
# Check if role exists
if not Role.objects.filter(name="Author").exists():
# Create it
role = Role.objects.create(
name="Author",
description="Can create and edit articles"
)
role.add_permission("articles.create")
role.add_permission("articles.edit")
# Now assign
from authz.services import assign_role
assign_role(user, "Author")
Issue: GraphQL mutations return "Permission denied"
Cause: GraphQL mutations require superuser access by default
Solution:
# Make sure the user making the request is a superuser
user.is_superuser = True
user.save()
# Or modify the mutation to check for a specific permission instead
# (requires editing the library code)
Issue: Template tag not working
Cause: Forgot to load the template tags
Solution:
{# Add this at the top of your template #}
{% load authz_tags %}
{# Now you can use the tags #}
{% if_has_permission 'articles.create' %}
...
{% endif_has_permission %}
Issue: Performance problems with many users/roles
Cause: N+1 query problem or missing indexes
Solution:
# Use select_related and prefetch_related
users = User.objects.prefetch_related(
'user_roles__role__permissions'
).all()
# Cache permission checks
from authz.engine import get_user_permission_codes
user_perms = get_user_permission_codes(user) # Cache this
# Check if permission is in cache
if "articles.create" in user_perms:
# Allow
pass
Issue: Policies not being evaluated
Cause: Policy not registered or permission code doesn't match
Solution:
from authz.policies import BasePolicy, register_policy
# Make sure you use the @register_policy decorator
@register_policy
class MyPolicy(BasePolicy):
# Make sure this matches exactly
permission_code = "articles.edit" # Must match what you're checking
def allows(self, user, obj=None, context=None):
return True
# Verify policy is registered
from authz.policies import policy_registry
print(policy_registry.get_policies("articles.edit"))
Debugging Tips
Enable logging:
# In settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'authz': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}
Check authorization flow:
from authz.engine import authorize
# Add print statements or use debugger
import pdb; pdb.set_trace()
result = authorize(user, "articles.create")
print(f"Authorization result: {result}")
Inspect user permissions:
from authz.services import get_user_permissions, get_permission_matrix
# Get all permissions with sources
perms = get_user_permissions(user)
for perm in perms:
print(f"{perm['code']} from {perm['source']}")
# Get complete matrix
matrix = get_permission_matrix(user)
print("Explicit allows:", matrix['explicit_allows'])
print("Explicit denies:", matrix['explicit_denies'])
print("Roles:", matrix['roles'])
print("All permissions:", matrix['all_permissions'])
License
MIT License. See LICENSE file for details.
Support and Contributing
Getting Help
- Documentation: You're reading it!
- Issues: Report bugs or request features on GitHub
- Questions: Open a discussion on GitHub
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Write tests for your changes
- Submit a pull request
Development Setup
# Clone the repository
git clone https://github.com/tarxemo/tarxemo-django-authz.git
cd tarxemo-django-authz
# Install in development mode
pip install -e .
# Install development dependencies
pip install -e .[drf]
# Run tests
python manage.py test authz
Made with ❤️ by TarXemo
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tarxemo_django_authz-0.2.2.tar.gz.
File metadata
- Download URL: tarxemo_django_authz-0.2.2.tar.gz
- Upload date:
- Size: 69.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0679fe7e4c3376fa0845c930d2fa07c9a3bb2855eb5e4b82be1c987c6aa4b749
|
|
| MD5 |
aa0b10dcf4a3719ab887e2174655cc4e
|
|
| BLAKE2b-256 |
fc9006dbd4d337a46bbf409d671e4b650633b9132b178488c0d9996bd8f79633
|
File details
Details for the file tarxemo_django_authz-0.2.2-py3-none-any.whl.
File metadata
- Download URL: tarxemo_django_authz-0.2.2-py3-none-any.whl
- Upload date:
- Size: 52.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
505ab3ca74d7cadbc280b95d1352c3b518e88c4257bc1f9b5f9e4a6f95150eb5
|
|
| MD5 |
7c5eb00b6289adb605b14bf51c7d6f7c
|
|
| BLAKE2b-256 |
96709e41ab863603ffc13c861a3a10e49e67f32bdfe5bf3f4c9c63cc029491d8
|