41 KiB
API Implementation Plan - Section 2: Standardize Authentication and Authorization
Date: 2025-01-XX
Status: Planning
Priority: High
Related Document: API-ENDPOINTS-ANALYSIS.md
Executive Summary
This document outlines the implementation plan for Section 2: Standardize Authentication and Authorization across the IGNY8 API layer. The goal is to ensure every API endpoint enforces consistent access control based on authenticated users, roles, and tenant/site scoping, improving security and maintainability.
Key Objectives:
- Standardize permission classes across all endpoints
- Implement consistent tenant/site-based access control
- Add role-based authorization where needed
- Secure custom actions and endpoints
- Ensure proper authentication fallback mechanisms
Table of Contents
- Current State Analysis
- Implementation Tasks
- Task 1: Define Global Permission Classes
- Task 2: Create Unified BaseViewSet with Permission Injection
- Task 3: Audit and Refactor All ViewSets
- Task 4: Inject Role-Based Checks Where Needed
- Task 5: Secure Custom Actions
- Task 6: Validate Token and Session Auth Coexistence
- Task 7: Frontend Sync + Fallback UX
- Task 8: Changelog Entry
- Testing Strategy
- Rollout Plan
- Success Criteria
Current State Analysis
Current Authentication & Authorization Issues
Based on API-ENDPOINTS-ANALYSIS.md, the following inconsistencies exist:
-
Inconsistent Permission Classes:
- Default:
AllowAny(most endpoints) - Many ViewSets set
permission_classes = [](explicit AllowAny) - Some use
IsAuthenticated - Some use custom classes:
IsOwnerOrAdmin,IsEditorOrAbove - No consistent pattern across modules
- Default:
-
Existing Permission Classes:
IsAuthenticated- DRF standardIsOwnerOrAdmin- Custom (owner or admin role)IsEditorOrAbove- Custom (editor, admin, or owner)IsViewerOrAbove- Custom (viewer or above)AccountPermission- Custom (account-based access)
-
Base Classes:
AccountModelViewSet- Has account filtering but no default permissionsSiteSectorModelViewSet- Has site/sector filtering but no default permissions
-
Authentication Methods:
- JWT Authentication (primary) -
igny8_core.api.authentication.JWTAuthentication - Session Authentication (fallback) -
CSRFExemptSessionAuthentication - Basic Authentication (fallback) -
rest_framework.authentication.BasicAuthentication
- JWT Authentication (primary) -
-
Account Context:
AccountContextMiddlewaresetsrequest.accountfrom JWT token- Account filtering happens in ViewSets but not consistently enforced
Implementation Tasks
Overview
| Task ID | Task Name | Priority | Estimated Effort | Dependencies |
|---|---|---|---|---|
| 1.1 | Create global permission classes | High | 3 hours | None |
| 2.1 | Create BaseTenantViewSet | High | 4 hours | 1.1 |
| 3.1 | Audit all ViewSets | Medium | 4 hours | None |
| 3.2 | Refactor Auth module ViewSets | High | 6 hours | 2.1 |
| 3.3 | Refactor Planner module ViewSets | High | 6 hours | 2.1 |
| 3.4 | Refactor Writer module ViewSets | High | 6 hours | 2.1 |
| 3.5 | Refactor System module ViewSets | High | 8 hours | 2.1 |
| 3.6 | Refactor Billing module ViewSets | High | 4 hours | 2.1 |
| 4.1 | Define role system | High | 2 hours | None |
| 4.2 | Create role-based permission classes | High | 4 hours | 4.1 |
| 4.3 | Apply role checks to elevated endpoints | High | 6 hours | 4.2 |
| 5.1 | Audit all custom actions | Medium | 4 hours | None |
| 5.2 | Secure custom actions | High | 8 hours | 1.1, 2.1 |
| 6.1 | Validate auth coexistence | High | 3 hours | None |
| 7.1 | Update frontend error handling | High | 4 hours | None |
| 7.2 | Update frontend role storage | Medium | 2 hours | None |
| 7.3 | Add UI guards | Medium | 4 hours | 7.2 |
| 8.1 | Create changelog entry | Low | 1 hour | All tasks |
Total Estimated Effort: ~75 hours
Task 1: Define Global Permission Classes
Goal
Create reusable permission classes that enforce consistent access control across all endpoints.
Implementation Steps
Step 1.1: Create Permission Classes Module
File: backend/igny8_core/api/permissions.py
Implementation:
"""
Unified Permission Classes for IGNY8 API
This module provides permission classes that enforce consistent
access control based on authentication, tenant access, and roles.
"""
from rest_framework import permissions
from rest_framework.exceptions import PermissionDenied
class IsAuthenticatedAndActive(permissions.BasePermission):
"""
Permission class that requires user to be authenticated and active.
This is the base permission for most endpoints. It ensures:
- User is authenticated
- User account is active
- User is not disabled
"""
def has_permission(self, request, view):
"""
Check if user is authenticated and active.
"""
if not request.user:
return False
if not request.user.is_authenticated:
return False
if not request.user.is_active:
return False
return True
def has_object_permission(self, request, view, obj):
"""
Object-level permission check.
By default, if user passes has_permission, allow object access.
Override in subclasses for object-level checks.
"""
return self.has_permission(request, view)
class HasTenantAccess(permissions.BasePermission):
"""
Permission class that ensures user has access to the tenant (account).
This permission:
- Requires user to be authenticated
- Checks that user belongs to the account (request.account)
- Allows system bots to bypass checks
- Validates account context is set
"""
def has_permission(self, request, view):
"""
Check if user has access to the tenant/account.
"""
if not request.user or not request.user.is_authenticated:
return False
# System bots can access all accounts
if hasattr(request.user, 'role') and request.user.role == 'system_bot':
return True
# Check if account context is set (from middleware or token)
account = getattr(request, 'account', None)
if not account:
# No account context - deny access
return False
# Check if user belongs to this account
user_account = getattr(request.user, 'account', None)
if not user_account:
return False
# User must belong to the account
return user_account == account
def has_object_permission(self, request, view, obj):
"""
Object-level permission: check if object belongs to user's account.
"""
if not request.user or not request.user.is_authenticated:
return False
# System bots can access all
if hasattr(request.user, 'role') and request.user.role == 'system_bot':
return True
# Check if object has account field
obj_account = getattr(obj, 'account', None)
if not obj_account:
# Object doesn't have account - allow (for non-account models)
return True
# Get user's account
user_account = getattr(request.user, 'account', None)
if not user_account:
return False
# Object must belong to user's account
return obj_account == user_account
class HasModuleAccess(permissions.BasePermission):
"""
Permission class that checks if user has access to a specific module.
This is optional and can be used for module-level access control
(e.g., Planner, Writer, System, Billing modules).
Usage:
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, HasModuleAccess]
# Then set module_name in ViewSet: module_name = 'planner'
"""
def has_permission(self, request, view):
"""
Check if user has access to the module.
"""
if not request.user or not request.user.is_authenticated:
return False
# Get module name from view
module_name = getattr(view, 'module_name', None)
if not module_name:
# No module restriction - allow
return True
# Check if user's account has access to this module
# This can be based on subscription, plan, or account settings
account = getattr(request, 'account', None)
if not account:
return False
# TODO: Implement module access check based on account settings
# For now, allow all authenticated users
# Example: return account.has_module_access(module_name)
return True
class IsAdminOrOwner(permissions.BasePermission):
"""
Permission class that requires user to have admin or owner role.
Use this for elevated privileges like:
- Credit management
- API key management
- User administration
- Account settings
"""
def has_permission(self, request, view):
"""
Check if user has admin or owner role.
"""
if not request.user or not request.user.is_authenticated:
return False
# Check user role
user_role = getattr(request.user, 'role', None)
if not user_role:
return False
# Allow admin or owner
return user_role in ['admin', 'owner']
def has_object_permission(self, request, view, obj):
"""
Object-level check: user must be admin or owner.
"""
return self.has_permission(request, view)
class IsEditorOrAbove(permissions.BasePermission):
"""
Permission class that requires editor, admin, or owner role.
Use this for content management operations.
"""
def has_permission(self, request, view):
"""
Check if user has editor, admin, or owner role.
"""
if not request.user or not request.user.is_authenticated:
return False
user_role = getattr(request.user, 'role', None)
if not user_role:
return False
# Allow editor, admin, or owner
return user_role in ['editor', 'admin', 'owner']
def has_object_permission(self, request, view, obj):
"""
Object-level check: user must be editor or above.
"""
return self.has_permission(request, view)
class IsViewerOrAbove(permissions.BasePermission):
"""
Permission class that requires viewer, editor, admin, or owner role.
Use this for read-only operations that should be accessible to all roles.
"""
def has_permission(self, request, view):
"""
Check if user has viewer, editor, admin, or owner role.
"""
if not request.user or not request.user.is_authenticated:
return False
user_role = getattr(request.user, 'role', None)
if not user_role:
return False
# Allow viewer, editor, admin, or owner
return user_role in ['viewer', 'editor', 'admin', 'owner']
def has_object_permission(self, request, view, obj):
"""
Object-level check: user must be viewer or above.
"""
return self.has_permission(request, view)
Step 1.2: Update __init__.py for Easy Import
File: backend/igny8_core/api/__init__.py
Add:
from .permissions import (
IsAuthenticatedAndActive,
HasTenantAccess,
HasModuleAccess,
IsAdminOrOwner,
IsEditorOrAbove,
IsViewerOrAbove,
)
__all__ = [
'IsAuthenticatedAndActive',
'HasTenantAccess',
'HasModuleAccess',
'IsAdminOrOwner',
'IsEditorOrAbove',
'IsViewerOrAbove',
]
Step 1.3: Create Unit Tests
File: backend/igny8_core/api/tests/test_permissions.py
Test Cases:
- Test
IsAuthenticatedAndActivewith authenticated/active user - Test
IsAuthenticatedAndActivewith unauthenticated user - Test
IsAuthenticatedAndActivewith inactive user - Test
HasTenantAccesswith matching account - Test
HasTenantAccesswith mismatched account - Test
HasTenantAccesswith system bot - Test
IsAdminOrOwnerwith admin role - Test
IsAdminOrOwnerwith owner role - Test
IsAdminOrOwnerwith editor role (should fail) - Test
IsEditorOrAbovewith various roles - Test object-level permissions
Estimated Time: 3 hours
Task 2: Create Unified BaseViewSet with Permission Injection
Goal
Create a base ViewSet class that automatically applies standard permissions and tenant filtering.
Implementation Steps
Step 2.1: Create BaseTenantViewSet
File: backend/igny8_core/api/viewsets.py
Implementation:
"""
Base ViewSet Classes with Unified Permissions and Tenant Filtering
"""
from rest_framework import viewsets
from igny8_core.api.permissions import (
IsAuthenticatedAndActive,
HasTenantAccess,
)
from igny8_core.api.base import AccountModelViewSet
class BaseTenantViewSet(AccountModelViewSet):
"""
Base ViewSet that automatically applies standard permissions and tenant filtering.
This ViewSet:
- Requires authentication and active user
- Enforces tenant/account access
- Automatically filters queryset by account
- Can be extended for module-specific or role-specific access
All module ViewSets should inherit from this instead of AccountModelViewSet.
Usage:
class MyViewSet(BaseTenantViewSet):
queryset = MyModel.objects.all()
serializer_class = MySerializer
"""
# Default permissions: require authentication and tenant access
permission_classes = [
IsAuthenticatedAndActive,
HasTenantAccess,
]
def get_queryset(self):
"""
Get queryset filtered by account.
This extends AccountModelViewSet.get_queryset() to ensure
proper account filtering is applied.
"""
queryset = super().get_queryset()
# Account filtering is handled by AccountModelViewSet
# This method can be overridden in subclasses for additional filtering
return queryset
def perform_create(self, serializer):
"""
Override to ensure account is set on create.
"""
# Get account from request (set by middleware or authentication)
account = getattr(self.request, 'account', None)
if account and hasattr(serializer.Meta.model, 'account'):
# Set account on the object being created
serializer.save(account=account)
else:
serializer.save()
Step 2.2: Update AccountModelViewSet (if needed)
File: backend/igny8_core/api/base.py
Review and ensure:
- Account filtering logic is correct
- Admin/developer override logic works
- System account bypass works correctly
- Compatible with new permission classes
Estimated Time: 4 hours
Task 3: Audit and Refactor All ViewSets
Goal
Update all ViewSets to inherit from BaseTenantViewSet and remove redundant permission logic.
Implementation Strategy
Step 3.1: Audit All ViewSets
Action Items:
- List all ViewSets in each module
- Document current permission classes
- Identify ViewSets that need role-based permissions
- Identify ViewSets that should remain public (AllowAny)
- Create refactoring checklist
Files to Audit:
Auth Module:
backend/auth/api/views.py- UsersViewSet, AccountsViewSet, SiteViewSet, etc.backend/auth/api/viewsets.py- Custom authentication views
Planner Module:
backend/planner/api/viewsets.py- KeywordViewSet, ClusterViewSet, ContentIdeasViewSet
Writer Module:
backend/writer/api/viewsets.py- TasksViewSet, ContentViewSet, ImagesViewSet
System Module:
backend/system/api/viewsets.py- AIPromptViewSet, AuthorProfileViewSet, StrategyViewSetbackend/system/api/viewsets.py- IntegrationSettingsViewSet, SystemSettingsViewSet, etc.
Billing Module:
backend/billing/api/viewsets.py- CreditBalanceViewSet, CreditUsageViewSet, CreditTransactionViewSet
Public Endpoints (should remain AllowAny):
POST /api/v1/auth/register/- RegistrationPOST /api/v1/auth/login/- LoginGET /api/v1/auth/plans/- Public plans listGET /api/v1/auth/industries/- Public industries listGET /api/v1/system/status/- System health check
Estimated Time: 4 hours
Step 3.2: Refactor Auth Module ViewSets
Priority Endpoints:
POST /api/v1/auth/register/- KeepAllowAny(public)POST /api/v1/auth/login/- KeepAllowAny(public)POST /api/v1/auth/change-password/- UseIsAuthenticatedAndActiveGET /api/v1/auth/me/- UseIsAuthenticatedAndActive- UsersViewSet - Use
IsAdminOrOwner(user management) - AccountsViewSet - Use
IsAdminOrOwner(account management) - SiteViewSet - Use
IsEditorOrAbove(site management) - SectorViewSet - Use
IsEditorOrAbove(sector management) - PlanViewSet - Keep
AllowAny(public read-only) - IndustryViewSet - Keep
AllowAny(public read-only)
Example Refactoring:
Before:
class UsersViewSet(AccountModelViewSet):
permission_classes = [IsOwnerOrAdmin] # Old custom class
queryset = User.objects.all()
serializer_class = UserSerializer
After:
from igny8_core.api.viewsets import BaseTenantViewSet
from igny8_core.api.permissions import IsAdminOrOwner
class UsersViewSet(BaseTenantViewSet):
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
queryset = User.objects.all()
serializer_class = UserSerializer
Estimated Time: 6 hours
Step 3.3: Refactor Planner Module ViewSets
Priority Endpoints:
- KeywordViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess - ClusterViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess - ContentIdeasViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess
Note: Planner module ViewSets inherit from SiteSectorModelViewSet, which already has account filtering. Update to use BaseTenantViewSet or create BaseSiteSectorViewSet that extends BaseTenantViewSet.
Estimated Time: 6 hours
Step 3.4: Refactor Writer Module ViewSets
Priority Endpoints:
- TasksViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess - ContentViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess - ImagesViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess
Estimated Time: 6 hours
Step 3.5: Refactor System Module ViewSets
Priority Endpoints:
- AIPromptViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess - AuthorProfileViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess - StrategyViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess - IntegrationSettingsViewSet - Use
IsAdminOrOwner(sensitive settings) - SystemSettingsViewSet - Use
IsAdminOrOwner(system settings) - AccountSettingsViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess - UserSettingsViewSet - Use
IsAuthenticatedAndActive(user-specific)
Estimated Time: 8 hours
Step 3.6: Refactor Billing Module ViewSets
Priority Endpoints:
- CreditBalanceViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess - CreditUsageViewSet - Use
IsAuthenticatedAndActive, HasTenantAccess - CreditTransactionViewSet - Use
IsAdminOrOwner(sensitive financial data)
Estimated Time: 4 hours
Refactoring Checklist Template
For each ViewSet, check:
- Inherit from
BaseTenantViewSet(or appropriate base class) - Set appropriate
permission_classes - Remove redundant permission logic
- Ensure queryset filtering works correctly
- Test authentication required
- Test tenant access enforcement
- Test role-based access (if applicable)
- Verify public endpoints remain public (AllowAny)
Task 4: Inject Role-Based Checks Where Needed
Goal
Implement role-based authorization for endpoints that require elevated privileges.
Implementation Steps
Step 4.1: Define Role System
Review existing role system:
File: backend/igny8_core/auth/models.py (or wherever User model is defined)
Roles to support:
owner- Full account accessadmin- Administrative accesseditor- Content editing accesswriter- Content writing accessviewer- Read-only accesssystem_bot- System/internal access
Action Items:
- Verify User model has
rolefield - Document role hierarchy and permissions
- Create role enum or constants if needed
Estimated Time: 2 hours
Step 4.2: Create Role-Based Permission Classes
File: backend/igny8_core/api/permissions.py (add to existing file)
Additional Permission Classes:
class RequireRole(permissions.BasePermission):
"""
Permission class that requires a specific role or set of roles.
Usage:
permission_classes = [IsAuthenticatedAndActive, RequireRole(['admin', 'owner'])]
"""
def __init__(self, allowed_roles):
self.allowed_roles = allowed_roles if isinstance(allowed_roles, list) else [allowed_roles]
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
user_role = getattr(request.user, 'role', None)
if not user_role:
return False
return user_role in self.allowed_roles
class RolePermissionMixin:
"""
Mixin that adds role-based permission checking to ViewSets.
Usage:
class MyViewSet(BaseTenantViewSet, RolePermissionMixin):
required_roles = ['admin', 'owner']
"""
def get_permissions(self):
"""
Add role-based permission if required_roles is set.
"""
permissions = super().get_permissions()
required_roles = getattr(self, 'required_roles', None)
if required_roles:
permissions.append(RequireRole(required_roles))
return permissions
Estimated Time: 4 hours
Step 4.3: Apply Role Checks to Elevated Endpoints
Endpoints requiring elevated privileges:
-
User Management:
POST /api/v1/auth/users/- Create user (admin/owner)PUT /api/v1/auth/users/{id}/- Update user (admin/owner)DELETE /api/v1/auth/users/{id}/- Delete user (admin/owner)POST /api/v1/auth/users/invite/- Invite user (admin/owner)
-
Account Management:
POST /api/v1/auth/accounts/- Create account (admin/owner)PUT /api/v1/auth/accounts/{id}/- Update account (admin/owner)DELETE /api/v1/auth/accounts/{id}/- Delete account (admin/owner)
-
Credit Management:
POST /api/v1/billing/credits/transactions/- Create transaction (admin/owner)- Credit balance updates (admin/owner)
-
Integration Settings:
POST /api/v1/system/settings/integrations/{pk}/save/- Save settings (admin/owner)POST /api/v1/system/settings/integrations/{pk}/test/- Test connection (admin/owner)
-
System Settings:
POST /api/v1/system/settings/system/- Create system setting (admin/owner)PUT /api/v1/system/settings/system/{id}/- Update system setting (admin/owner)
Example Application:
from igny8_core.api.viewsets import BaseTenantViewSet
from igny8_core.api.permissions import IsAdminOrOwner
class UsersViewSet(BaseTenantViewSet):
permission_classes = [
IsAuthenticatedAndActive,
HasTenantAccess,
IsAdminOrOwner, # Require admin or owner role
]
queryset = User.objects.all()
serializer_class = UserSerializer
Estimated Time: 6 hours
Task 5: Secure Custom Actions
Goal
Ensure all custom actions (@action methods) have proper permission checks and payload validation.
Implementation Steps
Step 5.1: Audit All Custom Actions
Action Items:
- List all
@actiondecorators in all ViewSets - Document current permission checks (if any)
- Identify actions that need explicit permissions
- Identify actions that need payload validation
Custom Actions to Audit:
Planner Module:
POST /api/v1/planner/keywords/bulk_delete/POST /api/v1/planner/keywords/bulk_update_status/POST /api/v1/planner/keywords/bulk_add_from_seed/POST /api/v1/planner/keywords/auto_cluster/POST /api/v1/planner/clusters/auto_generate_ideas/POST /api/v1/planner/ideas/bulk_queue_to_writer/
Writer Module:
POST /api/v1/writer/tasks/auto_generate_content/POST /api/v1/writer/content/generate_image_prompts/POST /api/v1/writer/images/generate_images/POST /api/v1/writer/images/bulk_update/
System Module:
POST /api/v1/system/prompts/save/POST /api/v1/system/prompts/reset/POST /api/v1/system/settings/integrations/{pk}/test/POST /api/v1/system/settings/integrations/{pk}/generate/
Estimated Time: 4 hours
Step 5.2: Secure Custom Actions
Implementation Pattern:
Before:
class KeywordViewSet(SiteSectorModelViewSet):
@action(detail=False, methods=['post'])
def bulk_delete(self, request):
ids = request.data.get('ids', [])
# No permission check, no validation
deleted_count = Keyword.objects.filter(id__in=ids).delete()[0]
return Response({"deleted_count": deleted_count})
After:
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess
class KeywordViewSet(BaseTenantViewSet):
@action(
detail=False,
methods=['post'],
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess]
)
def bulk_delete(self, request):
ids = request.data.get('ids', [])
# Validate payload
if not ids or not isinstance(ids, list):
return error_response(
error="Invalid payload: 'ids' must be a non-empty list",
status_code=status.HTTP_400_BAD_REQUEST
)
# Ensure all IDs belong to current user's account
queryset = self.get_queryset() # Already filtered by account
keywords = queryset.filter(id__in=ids)
# Validate all IDs exist and belong to account
if keywords.count() != len(ids):
return error_response(
error="Some keywords not found or don't belong to your account",
status_code=status.HTTP_403_FORBIDDEN
)
deleted_count = keywords.delete()[0]
return success_response(
data={"deleted_count": deleted_count},
message=f"Successfully deleted {deleted_count} keywords"
)
Key Security Checks:
- Explicit
permission_classeson@actiondecorator - Validate payload structure and types
- Use
get_queryset()to ensure account filtering - Verify all IDs belong to user's account before processing
- Return appropriate error responses for validation failures
Estimated Time: 8 hours
Task 6: Validate Token and Session Auth Coexistence
Goal
Ensure both token-based and session-based authentication work correctly and are used appropriately.
Implementation Steps
Step 6.1: Review Current Authentication Configuration
File: backend/igny8_core/settings.py
Current Configuration:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'igny8_core.api.authentication.JWTAuthentication', # Primary
'igny8_core.api.authentication.CSRFExemptSessionAuthentication', # Fallback
'rest_framework.authentication.BasicAuthentication', # Fallback
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny', # Will be changed to IsAuthenticatedAndActive
],
}
Action Items:
- Verify authentication classes are in correct order (JWT first)
- Ensure CSRFExemptSessionAuthentication is properly configured
- Document which endpoints should use which auth method
- Test authentication fallback behavior
Estimated Time: 2 hours
Step 6.2: Test Authentication Coexistence
Test Cases:
-
JWT Token Authentication:
- Valid JWT token in
Authorization: Bearer <token>header - Should authenticate successfully
- Should set
request.userandrequest.account
- Valid JWT token in
-
Session Authentication:
- Valid session cookie (from Django admin or session login)
- Should authenticate successfully as fallback
- Should work for admin panel (
/admin/)
-
Basic Authentication:
- Valid HTTP Basic Auth credentials
- Should authenticate as last fallback
- Should work for API testing tools
-
No Authentication:
- No token, no session, no basic auth
- Should return 401 Unauthorized for protected endpoints
- Should allow access for public endpoints (AllowAny)
-
Invalid Token:
- Expired JWT token
- Invalid JWT token format
- Should fall back to session auth, then basic auth
- Should return 401 if all fail
Estimated Time: 1 hour
Step 6.3: Document Authentication Usage
File: docs/04-BACKEND-IMPLEMENTATION.md (or create new auth doc)
Add Section: "Authentication Methods"
## Authentication Methods
### JWT Token Authentication (Primary)
- **Use Case:** Frontend application API calls
- **Header:** `Authorization: Bearer <token>`
- **Token Type:** Access token (15-minute expiry)
- **Token Payload:** `user_id`, `account_id`, `type: 'access'`
- **Account Context:** Automatically sets `request.account` from token
### Session Authentication (Fallback)
- **Use Case:** Django admin panel (`/admin/`)
- **Method:** Session cookies (CSRF exempt for API)
- **Configuration:** `CSRFExemptSessionAuthentication`
- **Note:** Only used when JWT authentication fails
### Basic Authentication (Fallback)
- **Use Case:** API testing tools (Postman, curl)
- **Method:** HTTP Basic Auth
- **Note:** Only used when JWT and session authentication fail
### Authentication Order
1. JWT Token Authentication (tried first)
2. Session Authentication (fallback)
3. Basic Authentication (last fallback)
4. If all fail: 401 Unauthorized
Estimated Time: 1 hour
Task 7: Frontend Sync + Fallback UX
Goal
Ensure frontend properly handles authentication errors and role-based UI restrictions.
Implementation Steps
Step 7.1: Update Frontend Error Handling
File: frontend/src/services/api.ts
Current Error Handling:
- Check for 401/403 responses
- Handle token refresh
- Route to login on authentication failure
Updates Needed:
-
Enhanced 401 Handling:
if (response.status === 401) { // Clear auth state authStore.getState().logout(); // Show user-friendly message showNotification('Session expired. Please log in again.', 'error'); // Redirect to login router.push('/login'); } -
Enhanced 403 Handling:
if (response.status === 403) { // Show permission error showNotification('You do not have permission to perform this action.', 'error'); // Optionally redirect or show restricted UI } -
Error Response Parsing:
// Parse unified error format const errorData = await response.json(); if (errorData.success === false) { const errorMessage = errorData.error || 'An error occurred'; const fieldErrors = errorData.errors || {}; // Handle error message and field errors }
Estimated Time: 4 hours
Step 7.2: Update Frontend Role Storage
File: frontend/src/stores/authStore.ts (or similar)
Updates Needed:
-
Store Role Information:
interface AuthState { user: { id: number; email: string; role: 'owner' | 'admin' | 'editor' | 'writer' | 'viewer'; account: { id: number; name: string; }; }; // ... other fields } -
Update on Login:
const login = async (email: string, password: string) => { const response = await fetchAPI('/v1/auth/login/', { method: 'POST', body: JSON.stringify({ email, password }), }); const data = await response.json(); if (data.success && data.user) { set({ user: data.user, token: data.tokens.access, // Store role for UI guards }); } }; -
Role Helper Functions:
export const hasRole = (requiredRole: string): boolean => { const user = authStore.getState().user; if (!user) return false; const roleHierarchy = ['viewer', 'writer', 'editor', 'admin', 'owner']; const userRoleIndex = roleHierarchy.indexOf(user.role); const requiredRoleIndex = roleHierarchy.indexOf(requiredRole); return userRoleIndex >= requiredRoleIndex; }; export const isAdminOrOwner = (): boolean => { const user = authStore.getState().user; return user?.role === 'admin' || user?.role === 'owner'; };
Estimated Time: 2 hours
Step 7.3: Add UI Guards
Implementation Pattern:
Component-Level Guards:
import { hasRole, isAdminOrOwner } from '@/utils/roles';
function UserManagementPage() {
const user = useAuthStore((state) => state.user);
// Check permission
if (!isAdminOrOwner()) {
return <AccessDenied message="Admin or Owner access required" />;
}
// Render component
return <UserManagement />;
}
Button-Level Guards:
function SettingsPage() {
const canManageSettings = isAdminOrOwner();
return (
<div>
<button
disabled={!canManageSettings}
onClick={handleSaveSettings}
>
Save Settings
</button>
{!canManageSettings && (
<span className="text-muted">Admin access required</span>
)}
</div>
);
}
Route-Level Guards:
// In router configuration
{
path: '/admin/users',
element: <ProtectedRoute requiredRole="admin"><UserManagement /></ProtectedRoute>,
}
Files to Update:
- Create
frontend/src/utils/roles.ts- Role helper functions - Create
frontend/src/components/ProtectedRoute.tsx- Route guard component - Create
frontend/src/components/AccessDenied.tsx- Access denied component - Update components that need role-based UI restrictions
Estimated Time: 4 hours
Task 8: Changelog Entry
Goal
Document the authentication and authorization standardization changes.
Implementation Steps
Step 8.1: Update CHANGELOG.md
File: CHANGELOG.md (or similar)
Add Entry:
## [Unreleased] - 2025-01-XX
### Changed
- **Authentication & Authorization**: Standardized authentication and permission enforcement across all API endpoints
- Created unified permission classes: `IsAuthenticatedAndActive`, `HasTenantAccess`, `IsAdminOrOwner`, `IsEditorOrAbove`, `IsViewerOrAbove`
- Created `BaseTenantViewSet` with automatic permission injection and tenant filtering
- Refactored all ViewSets across Auth, Planner, Writer, System, and Billing modules to use standardized permissions
- Secured all custom actions with explicit permission checks and payload validation
- Updated frontend error handling for 401/403 responses
- Added role-based UI guards and access controls
### Security
- **Breaking Change**: Most endpoints now require authentication (changed from `AllowAny` to `IsAuthenticatedAndActive`)
- Public endpoints (register, login, plans, industries, status) remain accessible without authentication
- All other endpoints now require valid JWT token or session authentication
- Tenant/account access is now enforced at the permission level
### Affected Areas
- API Layer (`igny8_core/api/permissions.py`, `igny8_core/api/viewsets.py`)
- Auth Module (`auth/api/`)
- Planner Module (`planner/api/`)
- Writer Module (`writer/api/`)
- System Module (`system/api/`)
- Billing Module (`billing/api/`)
- Frontend (`frontend/src/services/api.ts`, `frontend/src/stores/authStore.ts`)
### Migration Guide
1. **Frontend Updates Required:**
- Update API client to handle 401/403 errors gracefully
- Store user role information in auth store
- Add UI guards for role-based access
- Update error messages for authentication/permission failures
2. **API Testing:**
- All API tests must include authentication tokens
- Update test fixtures to use authenticated users
- Test role-based access for elevated endpoints
3. **Public Endpoints:**
- Registration, login, plans, industries, and status endpoints remain public
- All other endpoints require authentication
Estimated Time: 1 hour
Testing Strategy
Unit Tests
File: backend/igny8_core/api/tests/test_permissions.py
Test Cases:
- Permission classes with various user states
- Tenant access validation
- Role-based permission checks
- Object-level permissions
- System bot bypass logic
Integration Tests
Test Cases:
- End-to-end API calls with authentication
- Authentication failure scenarios (401)
- Permission denial scenarios (403)
- Tenant isolation (user can't access other accounts)
- Role-based access control
- Custom action security
Manual Testing
Checklist:
- All endpoints require authentication (except public ones)
- Tenant access is enforced correctly
- Role-based access works for elevated endpoints
- Custom actions have proper permission checks
- Payload validation works in custom actions
- Frontend handles 401/403 errors correctly
- UI guards hide/show features based on role
- Session auth works for admin panel
- JWT token auth works for API calls
Rollout Plan
Phase 1: Foundation (Week 1)
- ✅ Task 1: Create global permission classes
- ✅ Task 2: Create BaseTenantViewSet
- ✅ Unit tests for permission classes
Phase 2: Module Refactoring (Week 2-3)
- ✅ Task 3.2: Refactor Auth module
- ✅ Task 3.3: Refactor Planner module
- ✅ Task 3.4: Refactor Writer module
- ✅ Task 3.5: Refactor System module
- ✅ Task 3.6: Refactor Billing module
Phase 3: Role-Based Access (Week 4)
- ✅ Task 4: Implement role-based checks
- ✅ Task 5: Secure custom actions
- ✅ Integration testing
Phase 4: Frontend & Validation (Week 5)
- ✅ Task 6: Validate auth coexistence
- ✅ Task 7: Frontend updates
- ✅ End-to-end testing
- ✅ Bug fixes and adjustments
Phase 5: Documentation & Release (Week 6)
- ✅ Task 8: Changelog entry
- ✅ Documentation updates
- ✅ Release to staging
- ✅ Production deployment
Success Criteria
Definition of Done
- ✅ All permission classes are defined and tested
- ✅ BaseTenantViewSet is created and used by all ViewSets
- ✅ All ViewSets have appropriate permission classes
- ✅ All custom actions have explicit permission checks
- ✅ Payload validation is implemented in custom actions
- ✅ Frontend handles authentication errors correctly
- ✅ Role-based UI guards are implemented
- ✅ Documentation is updated
- ✅ Changelog entry is created
- ✅ All tests pass
Metrics
- Coverage: 100% of endpoints have explicit permission classes
- Security: All endpoints enforce tenant access (except public ones)
- Test Coverage: >90% for permission classes
- Breaking Changes: Documented and migration guide provided
Risk Assessment
Risks
-
Breaking Changes: Frontend may break if endpoints now require auth
- Mitigation: Keep public endpoints public, provide migration guide, test frontend early
-
Performance Impact: Permission checks may add overhead
- Mitigation: Minimal overhead, cache permission checks if needed
-
Incomplete Migration: Some endpoints may be missed
- Mitigation: Comprehensive audit checklist, automated tests
-
Role System Conflicts: Existing role system may conflict
- Mitigation: Review existing role implementation, ensure compatibility
Rollback Plan
If issues arise:
- Revert permission class changes (keep classes but don't apply)
- Keep BaseTenantViewSet (non-breaking)
- Gradually roll back ViewSet changes if needed
- Document issues for future fixes
Appendix
Permission Class Usage Guide
Public Endpoints (AllowAny)
class RegisterView(APIView):
permission_classes = [AllowAny] # Public endpoint
Standard Authenticated Endpoints
class MyViewSet(BaseTenantViewSet):
# Automatically uses IsAuthenticatedAndActive + HasTenantAccess
queryset = MyModel.objects.all()
Role-Based Endpoints
class UsersViewSet(BaseTenantViewSet):
permission_classes = [
IsAuthenticatedAndActive,
HasTenantAccess,
IsAdminOrOwner, # Require admin or owner
]
Custom Action with Explicit Permissions
class MyViewSet(BaseTenantViewSet):
@action(
detail=False,
methods=['post'],
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
)
def sensitive_action(self, request):
# Action implementation
pass
Document Status: Implementation Plan
Last Updated: 2025-01-XX
Next Review: After Phase 1 completion