Skip to content

Commit 1a3ddb4

Browse files
committed
refactor(#128): Extract repo resolution to helper functions with logging
- Move IndexedRepoData import to top level (was inline import) - Extract _resolve_repo_id() for cleaner search endpoint - Extract _validate_user_repo_access() for session ownership checks - Add structured logging for all auth scenarios: - Search on user-indexed repo (info) - Search denied - no session (warning) - Search denied - not owner (warning) - Search denied - repo expired (warning) - Demo repo searches (debug) - Truncate sensitive data in logs (session tokens, repo IDs) All 44 tests passing.
1 parent 9eb3f98 commit 1a3ddb4

1 file changed

Lines changed: 141 additions & 59 deletions

File tree

backend/routes/playground.py

Lines changed: 141 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from services.input_validator import InputValidator
1919
from services.repo_validator import RepoValidator
2020
from services.observability import logger
21-
from services.playground_limiter import PlaygroundLimiter, get_playground_limiter
21+
from services.playground_limiter import PlaygroundLimiter, get_playground_limiter, IndexedRepoData
2222
from services.anonymous_indexer import (
2323
AnonymousIndexingJob,
2424
run_indexing_job,
@@ -144,6 +144,145 @@ def _get_limiter() -> PlaygroundLimiter:
144144
return get_playground_limiter(redis_client)
145145

146146

147+
def _resolve_repo_id(
148+
request: PlaygroundSearchRequest,
149+
limiter: PlaygroundLimiter,
150+
limit_result,
151+
req: Request
152+
) -> str:
153+
"""
154+
Resolve which repository to search.
155+
156+
Priority: repo_id > demo_repo > default "flask"
157+
158+
For user-indexed repos, validates session ownership and expiry.
159+
Demo repos are always accessible without auth.
160+
161+
Returns:
162+
repo_id string
163+
164+
Raises:
165+
HTTPException 403: Access denied (not owner)
166+
HTTPException 410: Repo expired
167+
HTTPException 404: Demo repo not found
168+
"""
169+
# Case 1: Direct repo_id provided
170+
if request.repo_id:
171+
repo_id = request.repo_id
172+
173+
# Demo repos bypass auth check
174+
if repo_id in DEMO_REPO_IDS.values():
175+
logger.debug("Search on demo repo via repo_id", repo_id=repo_id[:16])
176+
return repo_id
177+
178+
# User-indexed repo - validate ownership
179+
return _validate_user_repo_access(repo_id, limiter, limit_result, req)
180+
181+
# Case 2: Fall back to demo_repo or default
182+
demo_name = request.demo_repo or "flask"
183+
repo_id = DEMO_REPO_IDS.get(demo_name)
184+
185+
if repo_id:
186+
logger.debug("Search on demo repo", demo_name=demo_name)
187+
return repo_id
188+
189+
# Case 3: Demo not in mapping, try first indexed repo
190+
repos = repo_manager.list_repos()
191+
indexed_repos = [r for r in repos if r.get("status") == "indexed"]
192+
193+
if indexed_repos:
194+
fallback_id = indexed_repos[0]["id"]
195+
logger.debug("Using fallback indexed repo", repo_id=fallback_id[:16])
196+
return fallback_id
197+
198+
logger.warning("No demo repo available", requested=demo_name)
199+
raise HTTPException(
200+
status_code=404,
201+
detail=f"Demo repo '{demo_name}' not available"
202+
)
203+
204+
205+
def _validate_user_repo_access(
206+
repo_id: str,
207+
limiter: PlaygroundLimiter,
208+
limit_result,
209+
req: Request
210+
) -> str:
211+
"""
212+
Validate that the session owns the requested user-indexed repo.
213+
214+
Returns:
215+
repo_id if valid
216+
217+
Raises:
218+
HTTPException 403: No session or not owner
219+
HTTPException 410: Repo expired
220+
"""
221+
session_token = limit_result.session_token or _get_session_token(req)
222+
token_preview = session_token[:8] if session_token else "none"
223+
224+
# No session token at all
225+
if not session_token:
226+
logger.warning(
227+
"Search denied - no session token",
228+
repo_id=repo_id[:16]
229+
)
230+
raise HTTPException(
231+
status_code=403,
232+
detail={
233+
"error": "access_denied",
234+
"message": "You don't have access to this repository"
235+
}
236+
)
237+
238+
# Get session data and check ownership
239+
session_data = limiter.get_session_data(session_token)
240+
indexed_repo = session_data.indexed_repo
241+
session_repo_id = indexed_repo.get("repo_id") if indexed_repo else None
242+
243+
if not indexed_repo or session_repo_id != repo_id:
244+
logger.warning(
245+
"Search denied - repo not owned by session",
246+
requested_repo_id=repo_id[:16],
247+
session_repo_id=session_repo_id[:16] if session_repo_id else "none",
248+
session_token=token_preview
249+
)
250+
raise HTTPException(
251+
status_code=403,
252+
detail={
253+
"error": "access_denied",
254+
"message": "You don't have access to this repository"
255+
}
256+
)
257+
258+
# Check expiry
259+
repo_data = IndexedRepoData.from_dict(indexed_repo)
260+
if repo_data.is_expired():
261+
logger.warning(
262+
"Search denied - repo expired",
263+
repo_id=repo_id[:16],
264+
expired_at=indexed_repo.get("expires_at"),
265+
session_token=token_preview
266+
)
267+
raise HTTPException(
268+
status_code=410,
269+
detail={
270+
"error": "repo_expired",
271+
"message": "Repository index expired. Re-index to continue searching.",
272+
"can_reindex": True
273+
}
274+
)
275+
276+
# All checks passed
277+
logger.info(
278+
"Search on user-indexed repo",
279+
repo_id=repo_id[:16],
280+
repo_name=indexed_repo.get("name"),
281+
session_token=token_preview
282+
)
283+
return repo_id
284+
285+
147286
@router.get("/limits")
148287
async def get_playground_limits(req: Request):
149288
"""
@@ -272,64 +411,7 @@ async def playground_search(
272411
raise HTTPException(status_code=400, detail=f"Invalid query: {query_error}")
273412

274413
# Resolve repo_id: priority is repo_id > demo_repo > default "flask"
275-
repo_id = None
276-
277-
if request.repo_id:
278-
# Direct repo_id provided
279-
repo_id = request.repo_id
280-
281-
# Check if it's a demo repo (always allowed)
282-
if repo_id not in DEMO_REPO_IDS.values():
283-
# User-indexed repo - validate session ownership
284-
session_token = limit_result.session_token or _get_session_token(req)
285-
if not session_token:
286-
raise HTTPException(
287-
status_code=403,
288-
detail={
289-
"error": "access_denied",
290-
"message": "You don't have access to this repository"
291-
}
292-
)
293-
294-
session_data = limiter.get_session_data(session_token)
295-
indexed_repo = session_data.indexed_repo
296-
297-
if not indexed_repo or indexed_repo.get("repo_id") != repo_id:
298-
raise HTTPException(
299-
status_code=403,
300-
detail={
301-
"error": "access_denied",
302-
"message": "You don't have access to this repository"
303-
}
304-
)
305-
306-
# Check expiry
307-
from services.playground_limiter import IndexedRepoData
308-
repo_data = IndexedRepoData.from_dict(indexed_repo)
309-
if repo_data.is_expired():
310-
raise HTTPException(
311-
status_code=410,
312-
detail={
313-
"error": "repo_expired",
314-
"message": "Repository index expired. Re-index to continue searching.",
315-
"can_reindex": True
316-
}
317-
)
318-
else:
319-
# Fall back to demo_repo (default to "flask" for backward compat)
320-
demo_name = request.demo_repo or "flask"
321-
repo_id = DEMO_REPO_IDS.get(demo_name)
322-
323-
if not repo_id:
324-
repos = repo_manager.list_repos()
325-
indexed_repos = [r for r in repos if r.get("status") == "indexed"]
326-
if indexed_repos:
327-
repo_id = indexed_repos[0]["id"]
328-
else:
329-
raise HTTPException(
330-
status_code=404,
331-
detail=f"Demo repo '{demo_name}' not available"
332-
)
414+
repo_id = _resolve_repo_id(request, limiter, limit_result, req)
333415

334416
start_time = time.time()
335417

0 commit comments

Comments
 (0)