Files
igny8/unified-api/API-IMPLEMENTATION-PLAN-SECTION2.md

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

  1. Current State Analysis
  2. Implementation Tasks
  3. Task 1: Define Global Permission Classes
  4. Task 2: Create Unified BaseViewSet with Permission Injection
  5. Task 3: Audit and Refactor All ViewSets
  6. Task 4: Inject Role-Based Checks Where Needed
  7. Task 5: Secure Custom Actions
  8. Task 6: Validate Token and Session Auth Coexistence
  9. Task 7: Frontend Sync + Fallback UX
  10. Task 8: Changelog Entry
  11. Testing Strategy
  12. Rollout Plan
  13. 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:

"""
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 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:

"""
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:

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:

  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:

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:

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:

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:

  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:

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 <token> 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"

## 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:

  1. 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');
    }
    
  2. 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
    }
    
  3. 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:

  1. Store Role Information:

    interface AuthState {
      user: {
        id: number;
        email: string;
        role: 'owner' | 'admin' | 'editor' | 'writer' | 'viewer';
        account: {
          id: number;
          name: string;
        };
      };
      // ... other fields
    }
    
  2. 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
        });
      }
    };
    
  3. 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

  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)

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