19 KiB
Content Strategy Authentication & Subscription Review
🎯 Executive Summary
This document reviews the content strategy feature's AI prompt calls to ensure they pass through main_text_generation with proper subscription and pre-flight checks. The review identified critical gaps where AI calls bypass subscription validation.
Review Date: January 2025
Status: ⚠️ CRITICAL ISSUES FOUND
🔍 Critical Findings
Issue 1: AI Calls Bypass Subscription Checks ❌ CRITICAL
Problem: Content strategy AI calls do NOT pass through main_text_generation with subscription checks.
Current Flow:
StrategyAnalyzer.call_ai_service()
→ AIServiceManager.execute_structured_json_call()
→ AIServiceManager._execute_ai_call()
→ AIServiceManager._call_gemini_structured()
→ gemini_structured_json_response() [DIRECT CALL - NO SUBSCRIPTION CHECK]
Expected Flow:
StrategyAnalyzer.call_ai_service(user_id)
→ AIServiceManager.execute_structured_json_call(user_id)
→ llm_text_gen(prompt, schema, user_id=user_id) [WITH SUBSCRIPTION CHECK]
Impact:
- ❌ No subscription limit enforcement
- ❌ No usage tracking
- ❌ No pre-flight validation
- ❌ Potential cost abuse
Issue 2: Missing User ID in AI Service Calls ❌ CRITICAL
Problem: AIServiceManager.execute_structured_json_call() does NOT accept or pass user_id.
Current Code:
# backend/services/ai_service_manager.py:553
async def execute_structured_json_call(self, service_type: AIServiceType, prompt: str, schema: Dict[str, Any]) -> Dict[str, Any]:
"""Public wrapper to execute a structured JSON AI call with a provided schema."""
return await self._execute_ai_call(service_type, prompt, schema)
Missing: user_id parameter
Impact: Cannot pass user_id to subscription checks even if we wanted to.
Issue 3: StrategyAnalyzer Doesn't Accept User ID ❌ CRITICAL
Problem: StrategyAnalyzer.call_ai_service() does NOT accept user_id parameter.
Current Code:
# backend/api/content_planning/services/content_strategy/ai_analysis/strategy_analyzer.py:327
async def call_ai_service(self, prompt: str, analysis_type: str) -> Dict[str, Any]:
# ... calls AIServiceManager without user_id
Missing: user_id parameter
Impact: Cannot pass user_id from strategy creation to AI calls.
Issue 4: Endpoints Don't Use Clerk Authentication ⚠️ HIGH PRIORITY
Problem: Content strategy endpoints accept user_id from request body instead of using Clerk authentication.
Current Code:
# backend/api/content_planning/api/content_strategy/endpoints/strategy_crud.py:38
@router.post("/create")
async def create_enhanced_strategy(
strategy_data: Dict[str, Any], # user_id comes from request body
db: Session = Depends(get_db)
) -> Dict[str, Any]:
Expected:
@router.post("/create")
async def create_enhanced_strategy(
strategy_data: Dict[str, Any],
current_user: Dict[str, Any] = Depends(get_current_user), # From Clerk
db: Session = Depends(get_db)
) -> Dict[str, Any]:
user_id = str(current_user.get('id', ''))
Impact:
- ⚠️ User can spoof user_id in request
- ⚠️ No authentication validation
- ⚠️ Security vulnerability
📊 Detailed Analysis
AI Call Flow Analysis
Current Implementation (BYPASSES SUBSCRIPTION)
# 1. StrategyAnalyzer calls AI service
async def call_ai_service(self, prompt: str, analysis_type: str):
ai_service = AIServiceManager()
response = await ai_service.execute_structured_json_call(
service_type, prompt, schema
# ❌ NO user_id passed
)
# 2. AIServiceManager executes call
async def execute_structured_json_call(self, service_type, prompt, schema):
return await self._execute_ai_call(service_type, prompt, schema)
# ❌ NO user_id parameter
# 3. Internal call uses direct Gemini provider
def _call_gemini_structured(self, prompt: str, schema: Dict[str, Any]):
return _gemini_fn(prompt, schema, ...)
# ❌ Calls gemini_structured_json_response DIRECTLY
# ❌ Bypasses llm_text_gen
# ❌ NO subscription checks
Expected Implementation (WITH SUBSCRIPTION)
# 1. StrategyAnalyzer calls AI service WITH user_id
async def call_ai_service(self, prompt: str, analysis_type: str, user_id: str):
ai_service = AIServiceManager()
response = await ai_service.execute_structured_json_call(
service_type, prompt, schema, user_id=user_id
# ✅ user_id passed
)
# 2. AIServiceManager executes call WITH user_id
async def execute_structured_json_call(self, service_type, prompt, schema, user_id: str):
return await self._execute_ai_call(service_type, prompt, schema, user_id=user_id)
# ✅ user_id parameter
# 3. Internal call uses llm_text_gen
def _call_llm_with_checks(self, prompt: str, schema: Dict[str, Any], user_id: str):
return llm_text_gen(
prompt=prompt,
json_struct=schema,
user_id=user_id # ✅ Passes user_id
)
# ✅ Uses llm_text_gen
# ✅ Has subscription checks
Subscription Check Flow
How llm_text_gen Works (CORRECT)
# backend/services/llm_providers/main_text_generation.py:19
def llm_text_gen(prompt: str, system_prompt: Optional[str] = None,
json_struct: Optional[Dict[str, Any]] = None,
user_id: str = None) -> str:
# ✅ SUBSCRIPTION CHECK - Required and strict enforcement
if not user_id:
raise RuntimeError("user_id is required for subscription checking.")
# ✅ Pre-flight validation
can_proceed, message, usage_info = pricing_service.check_usage_limits(
user_id=user_id,
provider=provider_enum,
tokens_requested=estimated_total_tokens
)
if not can_proceed:
raise RuntimeError(f"Subscription limit exceeded: {message}")
# ✅ Generate AI response
# ✅ Track usage after successful call
How Content Strategy Currently Works (INCORRECT)
# ❌ NO subscription check
# ❌ NO user_id validation
# ❌ NO usage tracking
# ❌ Direct Gemini API call
🔧 Required Fixes
Fix 1: Update AIServiceManager to Accept and Pass user_id
File: backend/services/ai_service_manager.py
Changes Required:
- Add
user_idparameter toexecute_structured_json_call() - Add
user_idparameter to_execute_ai_call() - Update
_call_gemini_structured()to usellm_text_gen()instead of direct Gemini call - Pass
user_idthrough the entire chain
Code Changes:
async def execute_structured_json_call(
self,
service_type: AIServiceType,
prompt: str,
schema: Dict[str, Any],
user_id: Optional[str] = None # ✅ ADD THIS
) -> Dict[str, Any]:
return await self._execute_ai_call(service_type, prompt, schema, user_id=user_id)
async def _execute_ai_call(
self,
service_type: AIServiceType,
prompt: str,
schema: Dict[str, Any],
user_id: Optional[str] = None # ✅ ADD THIS
) -> Dict[str, Any]:
# ✅ Use llm_text_gen instead of direct gemini call
response = await asyncio.wait_for(
asyncio.to_thread(
self._call_llm_with_checks, # ✅ CHANGE METHOD NAME
prompt,
schema,
user_id, # ✅ PASS user_id
),
timeout=self.config['timeout_seconds']
)
def _call_llm_with_checks(self, prompt: str, schema: Dict[str, Any], user_id: Optional[str] = None):
"""Call LLM through main_text_generation with subscription checks."""
from services.llm_providers.main_text_generation import llm_text_gen
if not user_id:
raise RuntimeError("user_id is required for subscription checking")
# ✅ Use llm_text_gen which has subscription checks
return llm_text_gen(
prompt=prompt,
json_struct=schema,
user_id=user_id # ✅ Pass user_id for subscription checks
)
Fix 2: Update StrategyAnalyzer to Accept and Pass user_id
File: backend/api/content_planning/services/content_strategy/ai_analysis/strategy_analyzer.py
Changes Required:
- Add
user_idparameter tocall_ai_service() - Add
user_idparameter togenerate_comprehensive_ai_recommendations() - Pass
user_idtoAIServiceManager.execute_structured_json_call()
Code Changes:
async def generate_comprehensive_ai_recommendations(
self,
strategy: EnhancedContentStrategy,
db: Session,
user_id: Optional[str] = None # ✅ ADD THIS
) -> None:
# Extract user_id from strategy if not provided
if not user_id:
user_id = str(strategy.user_id)
# ... existing code ...
recommendations = await self.generate_specialized_recommendations(
strategy, analysis_type, db, user_id=user_id # ✅ PASS user_id
)
async def generate_specialized_recommendations(
self,
strategy: EnhancedContentStrategy,
analysis_type: str,
db: Session,
user_id: Optional[str] = None # ✅ ADD THIS
) -> Dict[str, Any]:
# Extract user_id from strategy if not provided
if not user_id:
user_id = str(strategy.user_id)
prompt = self.create_specialized_prompt(strategy, analysis_type)
# ✅ Pass user_id to AI service call
ai_response = await self.call_ai_service(prompt, analysis_type, user_id=user_id)
async def call_ai_service(
self,
prompt: str,
analysis_type: str,
user_id: Optional[str] = None # ✅ ADD THIS
) -> Dict[str, Any]:
ai_service = AIServiceManager()
# ✅ Pass user_id to execute_structured_json_call
response = await ai_service.execute_structured_json_call(
service_type,
prompt,
schema,
user_id=user_id # ✅ PASS user_id
)
Fix 3: Update Content Strategy Endpoints to Use Clerk Authentication
File: backend/api/content_planning/api/content_strategy/endpoints/strategy_crud.py
Changes Required:
- Import
get_current_userfrom middleware - Add
current_userdependency to endpoints - Extract
user_idfrom Clerk user object - Validate
user_idmatches request body (if provided)
Code Changes:
# ✅ ADD IMPORT
from middleware.auth_middleware import get_current_user
@router.post("/create")
async def create_enhanced_strategy(
strategy_data: Dict[str, Any],
current_user: Dict[str, Any] = Depends(get_current_user), # ✅ ADD THIS
db: Session = Depends(get_db)
) -> Dict[str, Any]:
"""Create a new enhanced content strategy."""
try:
# ✅ Extract user_id from Clerk authentication
clerk_user_id = str(current_user.get('id', ''))
if not clerk_user_id:
raise HTTPException(
status_code=401,
detail="Invalid user ID in authentication token"
)
# ✅ Override user_id from request body with authenticated user_id
strategy_data['user_id'] = clerk_user_id
# ✅ Validate required fields
required_fields = ['name']
for field in required_fields:
if field not in strategy_data or not strategy_data[field]:
raise HTTPException(
status_code=400,
detail=f"Missing required field: {field}"
)
# ... rest of existing code ...
Apply Same Pattern To:
get_enhanced_strategies()- Filter by authenticated user_idget_enhanced_strategy_by_id()- Verify ownershipupdate_enhanced_strategy()- Verify ownershipdelete_enhanced_strategy()- Verify ownership
Fix 4: Update All Content Strategy Endpoints
Files to Update:
backend/api/content_planning/api/content_strategy/endpoints/strategy_crud.pybackend/api/content_planning/api/content_strategy/endpoints/ai_generation_endpoints.pybackend/api/content_planning/api/content_strategy/endpoints/autofill_endpoints.pybackend/api/content_planning/api/content_strategy/endpoints/streaming_endpoints.pybackend/api/content_planning/api/content_strategy/endpoints/analytics_endpoints.py
Pattern to Apply:
from middleware.auth_middleware import get_current_user
@router.post("/endpoint")
async def endpoint_function(
request_data: Dict[str, Any],
current_user: Dict[str, Any] = Depends(get_current_user), # ✅ ADD
db: Session = Depends(get_db)
):
# ✅ Extract authenticated user_id
user_id = str(current_user.get('id', ''))
if not user_id:
raise HTTPException(status_code=401, detail="Authentication required")
# ✅ Use authenticated user_id (override any from request)
# ✅ Pass user_id to all service calls
📋 Implementation Checklist
Phase 1: Core AI Service Fixes 🔴 CRITICAL
- Fix 1.1: Update
AIServiceManager.execute_structured_json_call()to acceptuser_id - Fix 1.2: Update
AIServiceManager._execute_ai_call()to acceptuser_id - Fix 1.3: Replace
_call_gemini_structured()with_call_llm_with_checks()usingllm_text_gen - Fix 1.4: Update all
AIServiceManagermethods to passuser_id
Phase 2: Strategy Analyzer Fixes 🔴 CRITICAL
- Fix 2.1: Update
StrategyAnalyzer.call_ai_service()to acceptuser_id - Fix 2.2: Update
StrategyAnalyzer.generate_comprehensive_ai_recommendations()to acceptuser_id - Fix 2.3: Update
StrategyAnalyzer.generate_specialized_recommendations()to acceptuser_id - Fix 2.4: Pass
user_idfrom strategy object when available
Phase 3: Endpoint Authentication 🟡 HIGH PRIORITY
- Fix 3.1: Add
get_current_usertostrategy_crud.pyendpoints - Fix 3.2: Add
get_current_usertoai_generation_endpoints.pyendpoints - Fix 3.3: Add
get_current_usertoautofill_endpoints.pyendpoints - Fix 3.4: Add
get_current_usertostreaming_endpoints.pyendpoints - Fix 3.5: Add
get_current_usertoanalytics_endpoints.pyendpoints - Fix 3.6: Update all endpoints to extract
user_idfrom Clerk authentication
Phase 4: Service Layer Updates 🟡 HIGH PRIORITY
- Fix 4.1: Update
EnhancedStrategyService.create_enhanced_strategy()to acceptuser_id - Fix 4.2: Update
EnhancedStrategyService.get_enhanced_strategies()to filter by authenticateduser_id - Fix 4.3: Update all service methods to use authenticated
user_id - Fix 4.4: Add ownership validation for update/delete operations
Phase 5: Testing & Validation 🟢 MEDIUM PRIORITY
- Fix 5.1: Test subscription limit enforcement
- Fix 5.2: Test usage tracking
- Fix 5.3: Test authentication enforcement
- Fix 5.4: Test user_id validation
- Fix 5.5: Verify all AI calls go through
llm_text_gen
🔄 Migration Strategy
Step 1: Update AIServiceManager (Backward Compatible)
- Add
user_idas optional parameter (defaults to None) - If
user_idis None, log warning but don't fail (for backward compatibility) - If
user_idis provided, usellm_text_genwith subscription checks - Gradually migrate all callers to provide
user_id
Step 2: Update StrategyAnalyzer
- Extract
user_idfrom strategy object - Pass
user_idto all AI service calls - Add fallback to strategy.user_id if not provided
Step 3: Update Endpoints
- Add
get_current_userdependency - Extract
user_idfrom Clerk authentication - Override any
user_idfrom request body - Pass authenticated
user_idto services
Step 4: Remove Backward Compatibility
- Make
user_idrequired inAIServiceManager - Make
user_idrequired inStrategyAnalyzer - Remove fallback logic
- Enforce authentication on all endpoints
📊 Impact Assessment
Security Impact 🔴 CRITICAL
- Current: Users can spoof
user_idin requests - Current: No subscription limit enforcement
- Current: No usage tracking
- After Fix: Proper authentication and authorization
- After Fix: Subscription limits enforced
- After Fix: Usage properly tracked
Cost Impact 🔴 CRITICAL
- Current: Unlimited AI calls without subscription checks
- Current: No cost tracking
- After Fix: Subscription limits prevent abuse
- After Fix: Proper cost tracking and billing
Functionality Impact 🟢 LOW
- Current: AI calls work but bypass checks
- After Fix: AI calls work WITH proper checks
- No Breaking Changes: Backward compatible migration path
🎯 Priority Actions
Immediate (This Week)
- ✅ Fix AIServiceManager - Add user_id support and use llm_text_gen
- ✅ Fix StrategyAnalyzer - Accept and pass user_id
- ✅ Fix strategy_crud.py - Add Clerk authentication
Short Term (Next Week)
- ✅ Fix all content strategy endpoints - Add authentication
- ✅ Update service layer - Use authenticated user_id
- ✅ Add ownership validation - Prevent unauthorized access
Medium Term (Next Sprint)
- ✅ Remove backward compatibility - Enforce user_id requirement
- ✅ Add comprehensive tests - Verify subscription checks
- ✅ Update documentation - Document authentication flow
📝 Code Examples
Before (INCORRECT)
# ❌ No authentication
@router.post("/create")
async def create_enhanced_strategy(
strategy_data: Dict[str, Any], # user_id from request body
db: Session = Depends(get_db)
):
user_id = strategy_data.get('user_id') # ❌ Can be spoofed
# ❌ AI call without subscription check
await strategy_analyzer.generate_comprehensive_ai_recommendations(strategy, db)
# ❌ No user_id passed
After (CORRECT)
# ✅ Clerk authentication
@router.post("/create")
async def create_enhanced_strategy(
strategy_data: Dict[str, Any],
current_user: Dict[str, Any] = Depends(get_current_user), # ✅ From Clerk
db: Session = Depends(get_db)
):
user_id = str(current_user.get('id', '')) # ✅ Authenticated
strategy_data['user_id'] = user_id # ✅ Override request body
# ✅ AI call WITH subscription check
await strategy_analyzer.generate_comprehensive_ai_recommendations(
strategy, db, user_id=user_id # ✅ Pass user_id
)
🔍 Verification Steps
After implementing fixes, verify:
- ✅ All content strategy endpoints require authentication
- ✅ All AI calls pass through
llm_text_genwithuser_id - ✅ Subscription limits are enforced
- ✅ Usage is tracked correctly
- ✅ Users cannot access other users' strategies
- ✅ Pre-flight validation works correctly
Last Updated: January 2025
Status: ⚠️ CRITICAL FIXES REQUIRED
Priority: 🔴 HIGHEST