# 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 1. [Current State Analysis](#current-state-analysis) 2. [Implementation Tasks](#implementation-tasks) 3. [Task 1: Define Global Permission Classes](#task-1-define-global-permission-classes) 4. [Task 2: Create Unified BaseViewSet with Permission Injection](#task-2-create-unified-baseviewset-with-permission-injection) 5. [Task 3: Audit and Refactor All ViewSets](#task-3-audit-and-refactor-all-viewsets) 6. [Task 4: Inject Role-Based Checks Where Needed](#task-4-inject-role-based-checks-where-needed) 7. [Task 5: Secure Custom Actions](#task-5-secure-custom-actions) 8. [Task 6: Validate Token and Session Auth Coexistence](#task-6-validate-token-and-session-auth-coexistence) 9. [Task 7: Frontend Sync + Fallback UX](#task-7-frontend-sync--fallback-ux) 10. [Task 8: Changelog Entry](#task-8-changelog-entry) 11. [Testing Strategy](#testing-strategy) 12. [Rollout Plan](#rollout-plan) 13. [Success Criteria](#success-criteria) --- ## Current State Analysis ### Current Authentication & Authorization Issues Based on `API-ENDPOINTS-ANALYSIS.md`, the following inconsistencies exist: 1. **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 2. **Existing Permission Classes:** - `IsAuthenticated` - DRF standard - `IsOwnerOrAdmin` - Custom (owner or admin role) - `IsEditorOrAbove` - Custom (editor, admin, or owner) - `IsViewerOrAbove` - Custom (viewer or above) - `AccountPermission` - Custom (account-based access) 3. **Base Classes:** - `AccountModelViewSet` - Has account filtering but no default permissions - `SiteSectorModelViewSet` - Has site/sector filtering but no default permissions 4. **Authentication Methods:** - JWT Authentication (primary) - `igny8_core.api.authentication.JWTAuthentication` - Session Authentication (fallback) - `CSRFExemptSessionAuthentication` - Basic Authentication (fallback) - `rest_framework.authentication.BasicAuthentication` 5. **Account Context:** - `AccountContextMiddleware` sets `request.account` from 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:** ```python """ 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:** ```python 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 `IsAuthenticatedAndActive` with authenticated/active user - Test `IsAuthenticatedAndActive` with unauthenticated user - Test `IsAuthenticatedAndActive` with inactive user - Test `HasTenantAccess` with matching account - Test `HasTenantAccess` with mismatched account - Test `HasTenantAccess` with system bot - Test `IsAdminOrOwner` with admin role - Test `IsAdminOrOwner` with owner role - Test `IsAdminOrOwner` with editor role (should fail) - Test `IsEditorOrAbove` with 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:** ```python """ 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:** 1. List all ViewSets in each module 2. Document current permission classes 3. Identify ViewSets that need role-based permissions 4. Identify ViewSets that should remain public (AllowAny) 5. 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, StrategyViewSet - `backend/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/` - Registration - `POST /api/v1/auth/login/` - Login - `GET /api/v1/auth/plans/` - Public plans list - `GET /api/v1/auth/industries/` - Public industries list - `GET /api/v1/system/status/` - System health check **Estimated Time:** 4 hours #### Step 3.2: Refactor Auth Module ViewSets **Priority Endpoints:** 1. `POST /api/v1/auth/register/` - Keep `AllowAny` (public) 2. `POST /api/v1/auth/login/` - Keep `AllowAny` (public) 3. `POST /api/v1/auth/change-password/` - Use `IsAuthenticatedAndActive` 4. `GET /api/v1/auth/me/` - Use `IsAuthenticatedAndActive` 5. UsersViewSet - Use `IsAdminOrOwner` (user management) 6. AccountsViewSet - Use `IsAdminOrOwner` (account management) 7. SiteViewSet - Use `IsEditorOrAbove` (site management) 8. SectorViewSet - Use `IsEditorOrAbove` (sector management) 9. PlanViewSet - Keep `AllowAny` (public read-only) 10. IndustryViewSet - Keep `AllowAny` (public read-only) **Example Refactoring:** **Before:** ```python class UsersViewSet(AccountModelViewSet): permission_classes = [IsOwnerOrAdmin] # Old custom class queryset = User.objects.all() serializer_class = UserSerializer ``` **After:** ```python 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:** 1. KeywordViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` 2. ClusterViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` 3. 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:** 1. TasksViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` 2. ContentViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` 3. ImagesViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` **Estimated Time:** 6 hours #### Step 3.5: Refactor System Module ViewSets **Priority Endpoints:** 1. AIPromptViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` 2. AuthorProfileViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` 3. StrategyViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` 4. IntegrationSettingsViewSet - Use `IsAdminOrOwner` (sensitive settings) 5. SystemSettingsViewSet - Use `IsAdminOrOwner` (system settings) 6. AccountSettingsViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` 7. UserSettingsViewSet - Use `IsAuthenticatedAndActive` (user-specific) **Estimated Time:** 8 hours #### Step 3.6: Refactor Billing Module ViewSets **Priority Endpoints:** 1. CreditBalanceViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` 2. CreditUsageViewSet - Use `IsAuthenticatedAndActive, HasTenantAccess` 3. 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 access - `admin` - Administrative access - `editor` - Content editing access - `writer` - Content writing access - `viewer` - Read-only access - `system_bot` - System/internal access **Action Items:** 1. Verify User model has `role` field 2. Document role hierarchy and permissions 3. 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:** ```python 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:** 1. **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) 2. **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) 3. **Credit Management:** - `POST /api/v1/billing/credits/transactions/` - Create transaction (admin/owner) - Credit balance updates (admin/owner) 4. **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) 5. **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:** ```python 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:** 1. List all `@action` decorators in all ViewSets 2. Document current permission checks (if any) 3. Identify actions that need explicit permissions 4. 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:** ```python 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:** ```python 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:** 1. Explicit `permission_classes` on `@action` decorator 2. Validate payload structure and types 3. Use `get_queryset()` to ensure account filtering 4. Verify all IDs belong to user's account before processing 5. 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:** ```python 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:** 1. Verify authentication classes are in correct order (JWT first) 2. Ensure CSRFExemptSessionAuthentication is properly configured 3. Document which endpoints should use which auth method 4. Test authentication fallback behavior **Estimated Time:** 2 hours #### Step 6.2: Test Authentication Coexistence **Test Cases:** 1. **JWT Token Authentication:** - Valid JWT token in `Authorization: Bearer ` header - Should authenticate successfully - Should set `request.user` and `request.account` 2. **Session Authentication:** - Valid session cookie (from Django admin or session login) - Should authenticate successfully as fallback - Should work for admin panel (`/admin/`) 3. **Basic Authentication:** - Valid HTTP Basic Auth credentials - Should authenticate as last fallback - Should work for API testing tools 4. **No Authentication:** - No token, no session, no basic auth - Should return 401 Unauthorized for protected endpoints - Should allow access for public endpoints (AllowAny) 5. **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"** ```markdown ## Authentication Methods ### JWT Token Authentication (Primary) - **Use Case:** Frontend application API calls - **Header:** `Authorization: Bearer ` - **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:** 1. **Enhanced 401 Handling:** ```typescript 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'); } ``` 2. **Enhanced 403 Handling:** ```typescript if (response.status === 403) { // Show permission error showNotification('You do not have permission to perform this action.', 'error'); // Optionally redirect or show restricted UI } ``` 3. **Error Response Parsing:** ```typescript // 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:** 1. **Store Role Information:** ```typescript interface AuthState { user: { id: number; email: string; role: 'owner' | 'admin' | 'editor' | 'writer' | 'viewer'; account: { id: number; name: string; }; }; // ... other fields } ``` 2. **Update on Login:** ```typescript 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 }); } }; ``` 3. **Role Helper Functions:** ```typescript 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:** ```typescript import { hasRole, isAdminOrOwner } from '@/utils/roles'; function UserManagementPage() { const user = useAuthStore((state) => state.user); // Check permission if (!isAdminOrOwner()) { return ; } // Render component return ; } ``` **Button-Level Guards:** ```typescript function SettingsPage() { const canManageSettings = isAdminOrOwner(); return (
{!canManageSettings && ( Admin access required )}
); } ``` **Route-Level Guards:** ```typescript // In router configuration { path: '/admin/users', element: , } ``` **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:** ```markdown ## [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 1. ✅ All permission classes are defined and tested 2. ✅ BaseTenantViewSet is created and used by all ViewSets 3. ✅ All ViewSets have appropriate permission classes 4. ✅ All custom actions have explicit permission checks 5. ✅ Payload validation is implemented in custom actions 6. ✅ Frontend handles authentication errors correctly 7. ✅ Role-based UI guards are implemented 8. ✅ Documentation is updated 9. ✅ Changelog entry is created 10. ✅ 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 1. **Breaking Changes:** Frontend may break if endpoints now require auth - **Mitigation:** Keep public endpoints public, provide migration guide, test frontend early 2. **Performance Impact:** Permission checks may add overhead - **Mitigation:** Minimal overhead, cache permission checks if needed 3. **Incomplete Migration:** Some endpoints may be missed - **Mitigation:** Comprehensive audit checklist, automated tests 4. **Role System Conflicts:** Existing role system may conflict - **Mitigation:** Review existing role implementation, ensure compatibility ### Rollback Plan If issues arise: 1. Revert permission class changes (keep classes but don't apply) 2. Keep BaseTenantViewSet (non-breaking) 3. Gradually roll back ViewSet changes if needed 4. Document issues for future fixes --- ## Appendix ### Permission Class Usage Guide #### Public Endpoints (AllowAny) ```python class RegisterView(APIView): permission_classes = [AllowAny] # Public endpoint ``` #### Standard Authenticated Endpoints ```python class MyViewSet(BaseTenantViewSet): # Automatically uses IsAuthenticatedAndActive + HasTenantAccess queryset = MyModel.objects.all() ``` #### Role-Based Endpoints ```python class UsersViewSet(BaseTenantViewSet): permission_classes = [ IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner, # Require admin or owner ] ``` #### Custom Action with Explicit Permissions ```python 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