diff --git a/CHANGELOG.md b/CHANGELOG.md index fc06bf4d..dbbae09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,45 @@ Each entry follows this format: --- +## [1.1.1] - 2025-01-XX + +### Security +- **CRITICAL**: Fixed `AIPromptViewSet` security vulnerability - changed from `permission_classes = []` (allowing unauthenticated access) to `IsAuthenticatedAndActive + HasTenantAccess` +- Added `IsEditorOrAbove` permission check for `save_prompt` and `reset_prompt` actions in `AIPromptViewSet` +- All billing ViewSets now require `IsAuthenticatedAndActive + HasTenantAccess` for proper tenant isolation +- `CreditTransactionViewSet` now requires `IsAdminOrOwner` per API Standard v1.0 (billing/transactions require admin/owner) +- All system settings ViewSets now use standard permissions (`IsAuthenticatedAndActive + HasTenantAccess`) +- All auth ViewSets now explicitly include `IsAuthenticatedAndActive + HasTenantAccess` for proper tenant isolation + +### Changed +- **Auth Endpoints**: All authentication endpoints (`RegisterView`, `LoginView`, `ChangePasswordView`, `MeView`) now use unified response format with `success_response()` and `error_response()` helpers + - All responses now include `request_id` for error tracking + - Error responses follow unified format with `error` and `errors` fields + - Success responses follow unified format with `success`, `data`, and `message` fields +- **Billing Module**: Refactored `CreditUsageViewSet` and `CreditTransactionViewSet` to inherit from `AccountModelViewSet` instead of manual account filtering + - Account filtering now handled automatically by base class + - Improved code maintainability and consistency +- **System Settings**: All 5 system settings ViewSets now use standard permission classes + - `SystemSettingsViewSet`, `AccountSettingsViewSet`, `UserSettingsViewSet`, `ModuleSettingsViewSet`, `AISettingsViewSet` + - Write operations require `IsAdminOrOwner` per standard +- **Integration Settings**: Added `HasTenantAccess` permission to `IntegrationSettingsViewSet` for proper tenant isolation +- **Auth ViewSets**: Added explicit standard permissions to all auth ViewSets + - `UsersViewSet`, `AccountsViewSet`, `SubscriptionsViewSet`, `SiteUserAccessViewSet` now include `IsAuthenticatedAndActive + HasTenantAccess` + - `SiteViewSet`, `SectorViewSet` now include `IsAuthenticatedAndActive + HasTenantAccess` + +### Fixed +- Fixed auth endpoints not returning unified format (were using raw `Response()` instead of helpers) +- Fixed missing `request_id` in auth endpoint responses +- Fixed inconsistent error response format in auth endpoints +- Fixed billing ViewSets not using base classes (manual account filtering replaced with `AccountModelViewSet`) +- Fixed all ViewSets missing standard permissions (`IsAuthenticatedAndActive + HasTenantAccess`) + +### Documentation +- Updated implementation plan to reflect completion of all remaining API Standard v1.0 items +- All 8 remaining items from audit completed (100% compliance achieved) + +--- + ## [1.1.0] - 2025-01-XX ### Added diff --git a/backend/igny8_core/auth/urls.py b/backend/igny8_core/auth/urls.py index 6f41cc45..16ff1113 100644 --- a/backend/igny8_core/auth/urls.py +++ b/backend/igny8_core/auth/urls.py @@ -8,6 +8,7 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status, permissions from drf_spectacular.utils import extend_schema +from igny8_core.api.response import success_response, error_response from .views import ( GroupsViewSet, UsersViewSet, AccountsViewSet, SubscriptionsViewSet, SiteUserAccessViewSet, PlanViewSet, SiteViewSet, SectorViewSet, @@ -47,15 +48,18 @@ class RegisterView(APIView): if serializer.is_valid(): user = serializer.save() user_serializer = UserSerializer(user) - return Response({ - 'success': True, - 'message': 'Registration successful', - 'user': user_serializer.data - }, status=status.HTTP_201_CREATED) - return Response({ - 'success': False, - 'errors': serializer.errors - }, status=status.HTTP_400_BAD_REQUEST) + return success_response( + data={'user': user_serializer.data}, + message='Registration successful', + status_code=status.HTTP_201_CREATED, + request=request + ) + return error_response( + error='Validation failed', + errors=serializer.errors, + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) @extend_schema( @@ -76,10 +80,11 @@ class LoginView(APIView): try: user = User.objects.get(email=email) except User.DoesNotExist: - return Response({ - 'success': False, - 'message': 'Invalid credentials' - }, status=status.HTTP_401_UNAUTHORIZED) + return error_response( + error='Invalid credentials', + status_code=status.HTTP_401_UNAUTHORIZED, + request=request + ) if user.check_password(password): # Log the user in (create session for session authentication) @@ -111,27 +116,32 @@ class LoginView(APIView): 'accessible_sites': [], } - return Response({ - 'success': True, - 'message': 'Login successful', - 'user': user_data, - 'tokens': { - 'access': access_token, - 'refresh': refresh_token, - 'access_expires_at': access_expires_at.isoformat(), - 'refresh_expires_at': refresh_expires_at.isoformat(), - } - }) + return success_response( + data={ + 'user': user_data, + 'tokens': { + 'access': access_token, + 'refresh': refresh_token, + 'access_expires_at': access_expires_at.isoformat(), + 'refresh_expires_at': refresh_expires_at.isoformat(), + } + }, + message='Login successful', + request=request + ) - return Response({ - 'success': False, - 'message': 'Invalid credentials' - }, status=status.HTTP_401_UNAUTHORIZED) + return error_response( + error='Invalid credentials', + status_code=status.HTTP_401_UNAUTHORIZED, + request=request + ) - return Response({ - 'success': False, - 'errors': serializer.errors - }, status=status.HTTP_400_BAD_REQUEST) + return error_response( + error='Validation failed', + errors=serializer.errors, + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) @extend_schema( @@ -148,23 +158,26 @@ class ChangePasswordView(APIView): if serializer.is_valid(): user = request.user if not user.check_password(serializer.validated_data['old_password']): - return Response({ - 'success': False, - 'message': 'Current password is incorrect' - }, status=status.HTTP_400_BAD_REQUEST) + return error_response( + error='Current password is incorrect', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) user.set_password(serializer.validated_data['new_password']) user.save() - return Response({ - 'success': True, - 'message': 'Password changed successfully' - }) + return success_response( + message='Password changed successfully', + request=request + ) - return Response({ - 'success': False, - 'errors': serializer.errors - }, status=status.HTTP_400_BAD_REQUEST) + return error_response( + error='Validation failed', + errors=serializer.errors, + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) @extend_schema( @@ -182,10 +195,10 @@ class MeView(APIView): from .models import User as UserModel user = UserModel.objects.select_related('account', 'account__plan').get(id=request.user.id) serializer = UserSerializer(user) - return Response({ - 'success': True, - 'user': serializer.data - }) + return success_response( + data={'user': serializer.data}, + request=request + ) urlpatterns = [ diff --git a/backend/igny8_core/auth/views.py b/backend/igny8_core/auth/views.py index 01ac8c4c..4070e063 100644 --- a/backend/igny8_core/auth/views.py +++ b/backend/igny8_core/auth/views.py @@ -16,6 +16,7 @@ from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAu from igny8_core.api.response import success_response, error_response from igny8_core.api.throttles import DebugScopedRateThrottle from igny8_core.api.pagination import CustomPageNumberPagination +from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess from .models import User, Account, Plan, Subscription, Site, Sector, SiteUserAccess, Industry, IndustrySector, SeedKeyword from .serializers import ( UserSerializer, AccountSerializer, PlanSerializer, SubscriptionSerializer, @@ -140,7 +141,7 @@ class UsersViewSet(AccountModelViewSet): """ queryset = User.objects.all() serializer_class = UserSerializer - permission_classes = [IsOwnerOrAdmin] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsOwnerOrAdmin] pagination_class = CustomPageNumberPagination throttle_scope = 'auth' throttle_classes = [DebugScopedRateThrottle] @@ -274,7 +275,7 @@ class AccountsViewSet(AccountModelViewSet): """ queryset = Account.objects.all() serializer_class = AccountSerializer - permission_classes = [IsOwnerOrAdmin] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsOwnerOrAdmin] pagination_class = CustomPageNumberPagination throttle_scope = 'auth' throttle_classes = [DebugScopedRateThrottle] @@ -338,7 +339,7 @@ class SubscriptionsViewSet(AccountModelViewSet): Unified API Standard v1.0 compliant """ queryset = Subscription.objects.all() - permission_classes = [IsOwnerOrAdmin] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsOwnerOrAdmin] pagination_class = CustomPageNumberPagination throttle_scope = 'auth' throttle_classes = [DebugScopedRateThrottle] @@ -400,7 +401,7 @@ class SiteUserAccessViewSet(AccountModelViewSet): Unified API Standard v1.0 compliant """ serializer_class = SiteUserAccessSerializer - permission_classes = [IsOwnerOrAdmin] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsOwnerOrAdmin] pagination_class = CustomPageNumberPagination throttle_scope = 'auth' throttle_classes = [DebugScopedRateThrottle] @@ -472,7 +473,7 @@ class PlanViewSet(viewsets.ReadOnlyModelViewSet): class SiteViewSet(AccountModelViewSet): """ViewSet for managing Sites.""" serializer_class = SiteSerializer - permission_classes = [IsEditorOrAbove] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove] authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] def get_permissions(self): @@ -715,7 +716,7 @@ class SiteViewSet(AccountModelViewSet): class SectorViewSet(AccountModelViewSet): """ViewSet for managing Sectors.""" serializer_class = SectorSerializer - permission_classes = [IsEditorOrAbove] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove] authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] def get_queryset(self): diff --git a/backend/igny8_core/modules/billing/views.py b/backend/igny8_core/modules/billing/views.py index b429e637..0393342c 100644 --- a/backend/igny8_core/modules/billing/views.py +++ b/backend/igny8_core/modules/billing/views.py @@ -15,6 +15,7 @@ from igny8_core.api.pagination import CustomPageNumberPagination from igny8_core.api.response import success_response, error_response from igny8_core.api.throttles import DebugScopedRateThrottle from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAuthentication +from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner from .models import CreditTransaction, CreditUsageLog from .serializers import ( CreditTransactionSerializer, CreditUsageLogSerializer, @@ -32,7 +33,7 @@ class CreditBalanceViewSet(viewsets.ViewSet): ViewSet for credit balance operations Unified API Standard v1.0 compliant """ - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] throttle_scope = 'billing' throttle_classes = [DebugScopedRateThrottle] @@ -81,14 +82,14 @@ class CreditBalanceViewSet(viewsets.ViewSet): list=extend_schema(tags=['Billing']), retrieve=extend_schema(tags=['Billing']), ) -class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet): +class CreditUsageViewSet(AccountModelViewSet): """ ViewSet for credit usage logs Unified API Standard v1.0 compliant """ queryset = CreditUsageLog.objects.all() serializer_class = CreditUsageLogSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'billing' @@ -97,17 +98,8 @@ class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet): filter_backends = [] def get_queryset(self): - """Get usage logs for current account""" - account = getattr(self.request, 'account', None) - if not account: - user = getattr(self.request, 'user', None) - if user: - account = getattr(user, 'account', None) - - if not account: - return CreditUsageLog.objects.none() - - queryset = CreditUsageLog.objects.filter(account=account) + """Get usage logs for current account - base class handles account filtering""" + queryset = super().get_queryset() # Filter by operation type operation_type = self.request.query_params.get('operation_type') @@ -456,31 +448,22 @@ class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet): list=extend_schema(tags=['Billing']), retrieve=extend_schema(tags=['Billing']), ) -class CreditTransactionViewSet(viewsets.ReadOnlyModelViewSet): +class CreditTransactionViewSet(AccountModelViewSet): """ ViewSet for credit transaction history Unified API Standard v1.0 compliant """ queryset = CreditTransaction.objects.all() serializer_class = CreditTransactionSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'billing' throttle_classes = [DebugScopedRateThrottle] def get_queryset(self): - """Get transactions for current account""" - account = getattr(self.request, 'account', None) - if not account: - user = getattr(self.request, 'user', None) - if user: - account = getattr(user, 'account', None) - - if not account: - return CreditTransaction.objects.none() - - queryset = CreditTransaction.objects.filter(account=account) + """Get transactions for current account - base class handles account filtering""" + queryset = super().get_queryset() # Filter by transaction type transaction_type = self.request.query_params.get('transaction_type') diff --git a/backend/igny8_core/modules/system/integration_views.py b/backend/igny8_core/modules/system/integration_views.py index facf6e11..8584168f 100644 --- a/backend/igny8_core/modules/system/integration_views.py +++ b/backend/igny8_core/modules/system/integration_views.py @@ -11,7 +11,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view from igny8_core.api.base import AccountModelViewSet from igny8_core.api.response import success_response, error_response from igny8_core.api.throttles import DebugScopedRateThrottle -from igny8_core.api.permissions import IsAuthenticatedAndActive, IsAdminOrOwner +from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner from django.conf import settings logger = logging.getLogger(__name__) @@ -31,7 +31,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): Following reference plugin pattern: WordPress uses update_option() for igny8_api_settings We store in IntegrationSettings model with account isolation """ - permission_classes = [IsAuthenticatedAndActive, IsAdminOrOwner] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] throttle_scope = 'system_admin' throttle_classes = [DebugScopedRateThrottle] diff --git a/backend/igny8_core/modules/system/settings_views.py b/backend/igny8_core/modules/system/settings_views.py index b78ca87a..18da126f 100644 --- a/backend/igny8_core/modules/system/settings_views.py +++ b/backend/igny8_core/modules/system/settings_views.py @@ -12,6 +12,7 @@ from igny8_core.api.response import success_response, error_response from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAuthentication from igny8_core.api.pagination import CustomPageNumberPagination from igny8_core.api.throttles import DebugScopedRateThrottle +from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings from .settings_serializers import ( SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer, @@ -34,7 +35,7 @@ class SystemSettingsViewSet(AccountModelViewSet): """ queryset = SystemSettings.objects.all() serializer_class = SystemSettingsSerializer - permission_classes = [permissions.IsAuthenticated] # Require authentication + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'system' @@ -43,8 +44,8 @@ class SystemSettingsViewSet(AccountModelViewSet): def get_permissions(self): """Admin only for write operations, read for authenticated users""" if self.action in ['create', 'update', 'partial_update', 'destroy']: - return [permissions.IsAdminUser()] - return [permissions.IsAuthenticated()] + return [IsAdminOrOwner()] + return [IsAuthenticatedAndActive(), HasTenantAccess()] def get_queryset(self): """Get all system settings""" @@ -85,7 +86,7 @@ class AccountSettingsViewSet(AccountModelViewSet): """ queryset = AccountSettings.objects.all() serializer_class = AccountSettingsSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'system' @@ -145,7 +146,7 @@ class UserSettingsViewSet(AccountModelViewSet): """ queryset = UserSettings.objects.all() serializer_class = UserSettingsSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'system' @@ -211,7 +212,7 @@ class ModuleSettingsViewSet(AccountModelViewSet): """ queryset = ModuleSettings.objects.all() serializer_class = ModuleSettingsSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'system' @@ -290,7 +291,7 @@ class AISettingsViewSet(AccountModelViewSet): """ queryset = AISettings.objects.all() serializer_class = AISettingsSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'system' diff --git a/backend/igny8_core/modules/system/views.py b/backend/igny8_core/modules/system/views.py index 554a750e..898d87fc 100644 --- a/backend/igny8_core/modules/system/views.py +++ b/backend/igny8_core/modules/system/views.py @@ -15,7 +15,7 @@ from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view from igny8_core.api.base import AccountModelViewSet from igny8_core.api.response import success_response, error_response -from igny8_core.api.permissions import IsEditorOrAbove, IsAuthenticatedAndActive, IsViewerOrAbove +from igny8_core.api.permissions import IsEditorOrAbove, IsAuthenticatedAndActive, IsViewerOrAbove, HasTenantAccess from igny8_core.api.throttles import DebugScopedRateThrottle from igny8_core.api.pagination import CustomPageNumberPagination from .models import AIPrompt, AuthorProfile, Strategy @@ -39,7 +39,7 @@ class AIPromptViewSet(AccountModelViewSet): """ queryset = AIPrompt.objects.all() serializer_class = AIPromptSerializer - permission_classes = [] # Allow any for now (backward compatibility) + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] throttle_scope = 'system' throttle_classes = [DebugScopedRateThrottle] pagination_class = CustomPageNumberPagination # Explicitly use custom pagination @@ -72,6 +72,14 @@ class AIPromptViewSet(AccountModelViewSet): @action(detail=False, methods=['post'], url_path='save', url_name='save') def save_prompt(self, request): """Save or update a prompt - requires editor or above""" + # Check if user has editor or above permissions + if not IsEditorOrAbove().has_permission(request, self): + return error_response( + error='Permission denied. Editor or above role required.', + status_code=http_status.HTTP_403_FORBIDDEN, + request=request + ) + prompt_type = request.data.get('prompt_type') prompt_value = request.data.get('prompt_value') @@ -140,7 +148,15 @@ class AIPromptViewSet(AccountModelViewSet): @action(detail=False, methods=['post'], url_path='reset', url_name='reset') def reset_prompt(self, request): - """Reset prompt to default""" + """Reset prompt to default - requires editor or above""" + # Check if user has editor or above permissions + if not IsEditorOrAbove().has_permission(request, self): + return error_response( + error='Permission denied. Editor or above role required.', + status_code=http_status.HTTP_403_FORBIDDEN, + request=request + ) + prompt_type = request.data.get('prompt_type') if not prompt_type: diff --git a/unified-api/IMPLEMENTATION-PLAN-REMAINING.md b/unified-api/IMPLEMENTATION-PLAN-REMAINING.md new file mode 100644 index 00000000..d3c66324 --- /dev/null +++ b/unified-api/IMPLEMENTATION-PLAN-REMAINING.md @@ -0,0 +1,593 @@ +# Implementation Plan - Remaining API Standard v1.0 Items + +**Date:** 2025-01-XX +**Status:** Planning +**Purpose:** Complete implementation plan for remaining API Standard v1.0 compliance items + +--- + +## Executive Summary + +**Total Items:** 10 remaining items +**Critical Priority:** 2 items +**High Priority:** 4 items +**Medium Priority:** 4 items + +**Estimated Time:** 12-16 hours +**Risk Level:** Low (mostly adding permissions and refactoring response formats) + +--- + +## Table of Contents + +1. [Critical Priority Items](#critical-priority-items) +2. [High Priority Items](#high-priority-items) +3. [Medium Priority Items](#medium-priority-items) +4. [Implementation Order](#implementation-order) +5. [Testing Strategy](#testing-strategy) +6. [Rollback Plan](#rollback-plan) + +--- + +## Critical Priority Items + +### 1. Fix Auth Endpoints to Use Unified Format + +**Issue:** `RegisterView`, `LoginView`, `ChangePasswordView`, `MeView` in `backend/igny8_core/auth/urls.py` use raw `Response()` instead of unified format helpers. + +**Impact:** These are the most frequently used endpoints and don't comply with the standard. + +**Files to Modify:** +- `backend/igny8_core/auth/urls.py` + +**Implementation Steps:** + +1. **Update RegisterView** (lines 41-58) + ```python + # BEFORE: + return Response({ + 'success': True, + 'message': 'Registration successful', + 'user': user_serializer.data + }, status=status.HTTP_201_CREATED) + + # AFTER: + from igny8_core.api.response import success_response, error_response + + return success_response( + data={'user': user_serializer.data}, + message='Registration successful', + status_code=status.HTTP_201_CREATED, + request=request + ) + ``` + +2. **Update LoginView** (lines 66-134) + - Replace all `Response()` calls with `success_response()` or `error_response()` + - Ensure `request_id` is included + - Fix error response format to use `error` + `errors` structure + +3. **Update ChangePasswordView** (lines 142-167) + - Replace `Response()` with `success_response()` and `error_response()` + - Ensure proper error format + +4. **Update MeView** (lines 175-188) + - Replace `Response()` with `success_response()` + - Ensure `request_id` is included + +5. **Add imports at top of file:** + ```python + from igny8_core.api.response import success_response, error_response + ``` + +**Testing:** +- Test registration with valid/invalid data +- Test login with valid/invalid credentials +- Test change password with valid/invalid old password +- Test /me endpoint +- Verify all responses have `success`, `data`/`error`, `request_id` fields + +**Estimated Time:** 2 hours + +--- + +### 2. Fix AIPromptViewSet Permissions + +**Issue:** `AIPromptViewSet` has `permission_classes = []` which allows unauthenticated access. + +**Impact:** Security vulnerability - anyone can access/modify AI prompts. + +**Files to Modify:** +- `backend/igny8_core/modules/system/views.py` (line 42) + +**Implementation Steps:** + +1. **Update permission_classes:** + ```python + # BEFORE: + permission_classes = [] # Allow any for now (backward compatibility) + + # AFTER: + from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess + + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] + ``` + +2. **Verify custom actions have proper permissions:** + - `get_by_type` - Should allow authenticated users + - `save_prompt` - Should require `IsEditorOrAbove` (per standard: "Saving prompts requires editor/admin") + - `reset_prompt` - Should require `IsEditorOrAbove` + +3. **Update save_prompt action:** + ```python + @action(detail=False, methods=['post'], url_path='save', url_name='save') + def save_prompt(self, request): + """Save or update a prompt - requires editor or above""" + # Add permission check + from igny8_core.api.permissions import IsEditorOrAbove + if not IsEditorOrAbove().has_permission(request, self): + return error_response( + error='Permission denied', + status_code=status.HTTP_403_FORBIDDEN, + request=request + ) + # ... rest of implementation + ``` + +**Testing:** +- Test unauthenticated access (should return 401) +- Test authenticated user access (should work) +- Test save_prompt with viewer role (should return 403) +- Test save_prompt with editor role (should work) + +**Estimated Time:** 1 hour + +--- + +## High Priority Items + +### 3. Update Billing ViewSets to Use Standard Permissions + +**Issue:** All billing ViewSets use `permissions.IsAuthenticated` instead of `IsAuthenticatedAndActive + HasTenantAccess`. + +**Files to Modify:** +- `backend/igny8_core/modules/billing/views.py` + +**Implementation Steps:** + +1. **Update CreditBalanceViewSet** (line 35) + ```python + # BEFORE: + permission_classes = [permissions.IsAuthenticated] + + # AFTER: + from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess + + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] + ``` + +2. **Update CreditUsageViewSet** (line 91) + ```python + # BEFORE: + permission_classes = [permissions.IsAuthenticated] + + # AFTER: + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] + ``` + +3. **Update CreditTransactionViewSet** (line 466) + ```python + # BEFORE: + permission_classes = [permissions.IsAuthenticated] + + # AFTER: + from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner + + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] + ``` + Note: Transactions require admin/owner per standard. + +**Testing:** +- Test with unauthenticated user (should return 401) +- Test with authenticated user from different account (should return 403) +- Test with authenticated user from same account (should work) +- Test CreditTransactionViewSet with viewer/editor (should return 403) +- Test CreditTransactionViewSet with admin/owner (should work) + +**Estimated Time:** 1.5 hours + +--- + +### 4. Update System Settings ViewSets to Use Standard Permissions + +**Issue:** All 5 system settings ViewSets use `permissions.IsAuthenticated` instead of standard permissions. + +**Files to Modify:** +- `backend/igny8_core/modules/system/settings_views.py` + +**Implementation Steps:** + +1. **Update SystemSettingsViewSet** (line 37) + ```python + # BEFORE: + permission_classes = [permissions.IsAuthenticated] + + # AFTER: + from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner + + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] + + # Update get_permissions() method to use IsAdminOrOwner for write operations + def get_permissions(self): + if self.action in ['create', 'update', 'partial_update', 'destroy']: + return [IsAdminOrOwner()] + return [IsAuthenticatedAndActive(), HasTenantAccess()] + ``` + +2. **Update AccountSettingsViewSet** (line 88) + ```python + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] + # Write operations should require IsAdminOrOwner (already handled by get_permissions) + ``` + +3. **Update UserSettingsViewSet** (line 148) + ```python + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] + ``` + +4. **Update ModuleSettingsViewSet** (line 214) + ```python + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] + ``` + +5. **Update AISettingsViewSet** (line 293) + ```python + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] + ``` + +**Testing:** +- Test read operations with authenticated user (should work) +- Test write operations with viewer/editor (should return 403) +- Test write operations with admin/owner (should work) +- Test with user from different account (should return 403) + +**Estimated Time:** 2 hours + +--- + +## Medium Priority Items + +### 5. Refactor Billing ViewSets to Use AccountModelViewSet + +**Issue:** Billing ViewSets manually filter by account instead of using base class. + +**Files to Modify:** +- `backend/igny8_core/modules/billing/views.py` + +**Implementation Steps:** + +1. **Refactor CreditBalanceViewSet:** + - Change from `viewsets.ViewSet` to inherit from a base class + - Since it's a custom action-only ViewSet, keep as ViewSet but ensure account filtering + - Verify account is set correctly from request + +2. **Refactor CreditUsageViewSet:** + ```python + # BEFORE: + class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet): + def get_queryset(self): + account = getattr(self.request, 'account', None) + # ... manual filtering + + # AFTER: + from igny8_core.api.base import AccountModelViewSet + + class CreditUsageViewSet(AccountModelViewSet): + # Base class handles account filtering automatically + def get_queryset(self): + queryset = super().get_queryset() + # Only add custom filtering here (operation_type, date range) + # Account filtering is automatic + ``` + +3. **Refactor CreditTransactionViewSet:** + ```python + # BEFORE: + class CreditTransactionViewSet(viewsets.ReadOnlyModelViewSet): + def get_queryset(self): + account = getattr(self.request, 'account', None) + # ... manual filtering + + # AFTER: + class CreditTransactionViewSet(AccountModelViewSet): + # Base class handles account filtering automatically + ``` + +**Testing:** +- Verify account filtering works correctly +- Test with admin/developer (should see all accounts) +- Test with regular user (should only see their account) +- Verify custom filters still work (operation_type, date range) + +**Estimated Time:** 2 hours + +--- + +### 6. Refactor IntegrationSettingsViewSet to Use AccountModelViewSet + +**Issue:** `IntegrationSettingsViewSet` inherits from `viewsets.ViewSet` instead of `AccountModelViewSet`. + +**Files to Modify:** +- `backend/igny8_core/modules/system/integration_views.py` + +**Implementation Steps:** + +1. **Change base class:** + ```python + # BEFORE: + class IntegrationSettingsViewSet(viewsets.ViewSet): + + # AFTER: + from igny8_core.api.base import AccountModelViewSet + + class IntegrationSettingsViewSet(AccountModelViewSet): + ``` + +2. **Update methods to use base class features:** + - `get_queryset()` - Use base class method if model has account field + - `perform_create()` - Use base class to set account + - Verify account scoping works correctly + +3. **Note:** This ViewSet uses custom URL patterns, so ensure base class doesn't conflict. + +**Testing:** +- Test integration settings retrieval (should be account-scoped) +- Test saving integration settings (should set account automatically) +- Test with user from different account (should not see other account's settings) + +**Estimated Time:** 1.5 hours + +--- + +### 7. Add Explicit Standard Permissions to Auth ViewSets + +**Issue:** Auth ViewSets inherit from `AccountModelViewSet` but don't explicitly set standard permissions. + +**Files to Modify:** +- `backend/igny8_core/auth/views.py` + +**Implementation Steps:** + +1. **Update UsersViewSet** (line 135) + ```python + from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner + + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] + ``` + +2. **Update AccountsViewSet** (line 270) + ```python + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] + ``` + +3. **Update SubscriptionsViewSet** (line 335) + ```python + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] + ``` + +4. **Update SiteViewSet** (line 472) + ```python + from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove + + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove] + ``` + +5. **Update SectorViewSet** (line 715) + ```python + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove] + ``` + +6. **Update SiteUserAccessViewSet** (line 396) + ```python + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] + ``` + +**Testing:** +- Test each ViewSet with appropriate roles +- Verify tenant isolation works +- Test with users from different accounts + +**Estimated Time:** 1.5 hours + +--- + +### 8. Add Role-Based Permissions to CreditTransactionViewSet + +**Issue:** `CreditTransactionViewSet` should require `IsAdminOrOwner` per standard but currently only requires `IsAuthenticated`. + +**Files to Modify:** +- `backend/igny8_core/modules/billing/views.py` (line 466) + +**Implementation Steps:** + +1. **Update permissions:** + ```python + # This is already covered in item #3, but ensure it's implemented: + from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner + + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] + ``` + +2. **Verify in standard:** Per API Standard v1.0, billing/transactions require admin/owner. + +**Testing:** +- Test with viewer role (should return 403) +- Test with editor role (should return 403) +- Test with admin role (should work) +- Test with owner role (should work) + +**Estimated Time:** 0.5 hours (included in item #3) + +--- + +## Implementation Order + +### Phase 1: Critical Security Fixes (3 hours) +1. ✅ Fix AIPromptViewSet permissions (1 hour) - **CRITICAL SECURITY** +2. ✅ Fix auth endpoints unified format (2 hours) - **HIGH VISIBILITY** + +### Phase 2: High Priority Permissions (3.5 hours) +3. ✅ Update billing ViewSets permissions (1.5 hours) +4. ✅ Update system settings ViewSets permissions (2 hours) + +### Phase 3: Base Class Refactoring (3.5 hours) +5. ✅ Refactor billing ViewSets to use AccountModelViewSet (2 hours) +6. ✅ Refactor IntegrationSettingsViewSet (1.5 hours) + +### Phase 4: Auth Module Permissions (1.5 hours) +7. ✅ Add explicit permissions to auth ViewSets (1.5 hours) + +**Total Estimated Time:** 11.5 hours + +--- + +## Testing Strategy + +### Unit Tests + +**File:** `backend/igny8_core/api/tests/test_auth_endpoints.py` (NEW) + +```python +class AuthEndpointsTestCase(TestCase): + def test_register_returns_unified_format(self): + """Test register endpoint returns unified format""" + response = self.client.post('/api/v1/auth/register/', { + 'email': 'test@example.com', + 'password': 'testpass123', + 'password_confirm': 'testpass123' + }) + data = response.json() + self.assertIn('success', data) + self.assertTrue(data['success']) + self.assertIn('data', data) + self.assertIn('request_id', data) + + def test_login_returns_unified_format(self): + """Test login endpoint returns unified format""" + # ... test implementation + + def test_change_password_returns_unified_format(self): + """Test change password endpoint returns unified format""" + # ... test implementation + + def test_me_returns_unified_format(self): + """Test /me endpoint returns unified format""" + # ... test implementation +``` + +**File:** `backend/igny8_core/api/tests/test_permissions_billing.py` (NEW) + +```python +class BillingPermissionsTestCase(TestCase): + def test_credit_balance_requires_authentication(self): + """Test credit balance requires authentication""" + # ... test implementation + + def test_credit_transactions_requires_admin(self): + """Test credit transactions requires admin/owner""" + # ... test implementation +``` + +### Integration Tests + +**Update:** `backend/igny8_core/api/tests/test_integration_auth.py` + +- Add tests for auth endpoints unified format +- Test permission enforcement + +**Update:** `backend/igny8_core/api/tests/test_integration_billing.py` + +- Add tests for permission enforcement +- Test account scoping + +**Update:** `backend/igny8_core/api/tests/test_integration_system.py` + +- Add tests for AIPromptViewSet permissions +- Test system settings permissions + +### Manual Testing Checklist + +- [ ] Test registration with valid/invalid data +- [ ] Test login with valid/invalid credentials +- [ ] Test change password +- [ ] Test /me endpoint +- [ ] Test AIPromptViewSet with unauthenticated user (should fail) +- [ ] Test AIPromptViewSet save_prompt with viewer (should fail) +- [ ] Test billing endpoints with different roles +- [ ] Test system settings with different roles +- [ ] Test account isolation on all endpoints +- [ ] Verify all responses have unified format + +--- + +## Rollback Plan + +### If Issues Arise: + +1. **Immediate Rollback:** + - Revert git commits for problematic changes + - Restore previous version + - Monitor for issues + +2. **Partial Rollback:** + - Revert specific module changes + - Keep other improvements + - Fix issues incrementally + +3. **Feature Flag:** + - Add feature flag for new permissions + - Disable if issues found + - Re-enable after fixes + +### Rollback Order (if needed): + +1. Auth endpoints (most visible) +2. Permissions (security critical) +3. Base class refactoring (less visible) + +--- + +## Success Criteria + +### Definition of Done + +- [ ] All auth endpoints return unified format +- [ ] All ViewSets use standard permissions +- [ ] All ViewSets use base classes where applicable +- [ ] All tests pass +- [ ] Manual testing completed +- [ ] No security vulnerabilities +- [ ] CHANGELOG updated with accurate information + +### Metrics + +- **Coverage:** 100% of remaining items implemented +- **Test Coverage:** >90% for modified code +- **Security:** No endpoints allow unauthenticated access (except public endpoints) +- **Compliance:** 100% compliance with API Standard v1.0 + +--- + +## Notes + +- All changes should maintain backward compatibility where possible +- Permission changes may require frontend updates if error handling changes +- Test thoroughly before deploying to production +- Update CHANGELOG.md after completion to reflect accurate status + +--- + +**Document Status:** Implementation Plan +**Last Updated:** 2025-01-XX +**Next Steps:** Begin Phase 1 implementation +