@@ -83,6 +83,29 @@ async def dispatch(self, request: Request, call_next):
8383api_key_manager = APIKeyManager (get_supabase_service ().client )
8484cost_controller = CostController (get_supabase_service ().client )
8585
86+
87+ # ===== SECURITY HELPERS =====
88+
89+ def get_repo_or_404 (repo_id : str , user_id : str ) -> dict :
90+ """
91+ Get repository with ownership verification.
92+ Returns 404 if repo doesn't exist OR if user doesn't own it.
93+ (We return 404 instead of 403 to not leak info about repo existence)
94+ """
95+ repo = repo_manager .get_repo_for_user (repo_id , user_id )
96+ if not repo :
97+ raise HTTPException (status_code = 404 , detail = "Repository not found" )
98+ return repo
99+
100+
101+ def verify_repo_access (repo_id : str , user_id : str ) -> None :
102+ """
103+ Verify user has access to repository.
104+ Raises 404 if no access (not 403, to avoid leaking repo existence).
105+ """
106+ if not repo_manager .verify_ownership (repo_id , user_id ):
107+ raise HTTPException (status_code = 404 , detail = "Repository not found" )
108+
86109# Request/Response Models
87110class SearchRequest (BaseModel ):
88111 query : str
@@ -272,9 +295,11 @@ async def list_repositories(auth: AuthContext = Depends(require_auth)):
272295 """List all repositories for authenticated user"""
273296 user_id = auth .user_id
274297
275- # TODO: Filter repos by user_id once we add user_id column to repositories table
276- # For now, return all repos (will fix in next section)
277- repos = repo_manager .list_repos ()
298+ if not user_id :
299+ raise HTTPException (status_code = 401 , detail = "User ID required" )
300+
301+ # Only return repos owned by this user
302+ repos = repo_manager .list_repos_for_user (user_id )
278303 return {"repositories" : repos }
279304
280305
@@ -369,16 +394,18 @@ async def websocket_index(websocket: WebSocket, repo_id: str):
369394 if not user :
370395 return
371396
372- # TODO: Add repo ownership validation once user_id column exists in repos table
373- # For now, any authenticated user can index any repo they know the ID of
397+ user_id = user .get ("user_id" )
398+ if not user_id :
399+ await websocket .close (code = 4001 , reason = "User ID required" )
400+ return
374401
375- # Validate repo exists before accepting connection
376- repo = repo_manager .get_repo (repo_id )
402+ # Verify user owns this repository (return same error to not leak info)
403+ repo = repo_manager .get_repo_for_user (repo_id , user_id )
377404 if not repo :
378405 await websocket .close (code = 4004 , reason = "Repository not found" )
379406 return
380407
381- # Connection authenticated and repo valid - accept
408+ # Connection authenticated and repo ownership verified - accept
382409 await websocket .accept ()
383410
384411 try :
@@ -432,9 +459,8 @@ async def index_repository(
432459 start_time = time .time ()
433460
434461 try :
435- repo = repo_manager .get_repo (repo_id )
436- if not repo :
437- raise HTTPException (status_code = 404 , detail = "Repository not found" )
462+ # Verify ownership - returns 404 if not owned
463+ repo = get_repo_or_404 (repo_id , auth .user_id )
438464
439465 # Set status to indexing
440466 repo_manager .update_status (repo_id , "indexing" )
@@ -486,6 +512,9 @@ async def search_code(
486512):
487513 """Search code semantically with caching and validation"""
488514
515+ # Verify user owns the repository
516+ verify_repo_access (request .repo_id , auth .user_id )
517+
489518 # Validate search query
490519 valid_query , query_error = InputValidator .validate_search_query (request .query )
491520 if not valid_query :
@@ -534,9 +563,8 @@ async def explain_code(
534563 """Generate code explanation"""
535564
536565 try :
537- repo = repo_manager .get_repo (request .repo_id )
538- if not repo :
539- raise HTTPException (status_code = 404 , detail = "Repository not found" )
566+ # Verify ownership
567+ repo = get_repo_or_404 (request .repo_id , auth .user_id )
540568
541569 explanation = await indexer .explain_code (
542570 repo_id = request .repo_id ,
@@ -545,6 +573,8 @@ async def explain_code(
545573 )
546574
547575 return {"explanation" : explanation }
576+ except HTTPException :
577+ raise
548578 except Exception as e :
549579 raise HTTPException (status_code = 500 , detail = str (e ))
550580
@@ -565,9 +595,8 @@ async def get_dependency_graph(
565595 """Get dependency graph for repository with Supabase caching"""
566596
567597 try :
568- repo = repo_manager .get_repo (repo_id )
569- if not repo :
570- raise HTTPException (status_code = 404 , detail = "Repository not found" )
598+ # Verify ownership
599+ repo = get_repo_or_404 (repo_id , auth .user_id )
571600
572601 # Try loading from Supabase cache
573602 cached_graph = dependency_analyzer .load_from_cache (repo_id )
@@ -598,9 +627,8 @@ async def analyze_impact(
598627 """Analyze impact of changing a file with validation and caching"""
599628
600629 try :
601- repo = repo_manager .get_repo (repo_id )
602- if not repo :
603- raise HTTPException (status_code = 404 , detail = "Repository not found" )
630+ # Verify ownership
631+ repo = get_repo_or_404 (repo_id , auth .user_id )
604632
605633 # Validate file path
606634 valid_path , path_error = InputValidator .validate_file_path (request .file_path , repo ["local_path" ])
@@ -637,9 +665,8 @@ async def get_repository_insights(
637665 """Get comprehensive insights about repository with Supabase caching"""
638666
639667 try :
640- repo = repo_manager .get_repo (repo_id )
641- if not repo :
642- raise HTTPException (status_code = 404 , detail = "Repository not found" )
668+ # Verify ownership
669+ repo = get_repo_or_404 (repo_id , auth .user_id )
643670
644671 # Try loading cached graph from Supabase
645672 graph_data = dependency_analyzer .load_from_cache (repo_id )
@@ -679,9 +706,8 @@ async def get_style_analysis(
679706 """Analyze code style and team patterns with Supabase caching"""
680707
681708 try :
682- repo = repo_manager .get_repo (repo_id )
683- if not repo :
684- raise HTTPException (status_code = 404 , detail = "Repository not found" )
709+ # Verify ownership
710+ repo = get_repo_or_404 (repo_id , auth .user_id )
685711
686712 # Try loading from Supabase cache
687713 cached_style = style_analyzer .load_from_cache (repo_id )
0 commit comments