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

1358 lines
41 KiB
Markdown

# 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 <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"**
```markdown
## 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:**
```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 <AccessDenied message="Admin or Owner access required" />;
}
// Render component
return <UserManagement />;
}
```
**Button-Level Guards:**
```typescript
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:**
```typescript
// 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:**
```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