|
18 | 18 | from services.input_validator import InputValidator |
19 | 19 | from services.repo_validator import RepoValidator |
20 | 20 | 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 |
22 | 22 | from services.anonymous_indexer import ( |
23 | 23 | AnonymousIndexingJob, |
24 | 24 | run_indexing_job, |
@@ -144,6 +144,145 @@ def _get_limiter() -> PlaygroundLimiter: |
144 | 144 | return get_playground_limiter(redis_client) |
145 | 145 |
|
146 | 146 |
|
| 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 | + |
147 | 286 | @router.get("/limits") |
148 | 287 | async def get_playground_limits(req: Request): |
149 | 288 | """ |
@@ -272,64 +411,7 @@ async def playground_search( |
272 | 411 | raise HTTPException(status_code=400, detail=f"Invalid query: {query_error}") |
273 | 412 |
|
274 | 413 | # 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) |
333 | 415 |
|
334 | 416 | start_time = time.time() |
335 | 417 |
|
|
0 commit comments