Skip to content

Commit fc04818

Browse files
authored
Merge pull request #730 from jsbattig/fix/golden-repo-add-index-auth
Fix/golden repo add index auth
2 parents defc730 + 711f501 commit fc04818

56 files changed

Lines changed: 1218 additions & 626 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/code_indexer/server/app.py

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,18 +1931,7 @@ def create_app() -> FastAPI:
19311931
Returns:
19321932
Configured FastAPI app
19331933
"""
1934-
global \
1935-
jwt_manager, \
1936-
user_manager, \
1937-
refresh_token_manager, \
1938-
golden_repo_manager, \
1939-
background_job_manager, \
1940-
activated_repo_manager, \
1941-
repository_listing_manager, \
1942-
semantic_query_manager, \
1943-
_server_start_time, \
1944-
_server_hnsw_cache, \
1945-
_server_fts_cache
1934+
global jwt_manager, user_manager, refresh_token_manager, golden_repo_manager, background_job_manager, activated_repo_manager, repository_listing_manager, semantic_query_manager, _server_start_time, _server_hnsw_cache, _server_fts_cache
19461935

19471936
# Story #526: Initialize server-side HNSW cache at bootstrap for 1800x performance
19481937
# Import and initialize global cache instance
@@ -4233,9 +4222,12 @@ async def refresh_golden_repo(
42334222
status_code=202,
42344223
)
42354224
async def add_golden_repo_index(
4225+
http_request: Request,
42364226
alias: str,
42374227
request: AddIndexRequest,
4238-
current_user: dependencies.User = Depends(dependencies.get_current_admin_user),
4228+
current_user: dependencies.User = Depends(
4229+
dependencies.get_current_admin_user_hybrid
4230+
),
42394231
):
42404232
"""
42414233
Add an index type to a golden repository (admin only) - async operation.
@@ -4338,8 +4330,9 @@ async def get_golden_repo_index_status(
43384330

43394331
@app.get("/api/jobs/{job_id}", response_model=JobStatusResponse)
43404332
async def get_job_status(
4333+
http_request: Request,
43414334
job_id: str,
4342-
current_user: dependencies.User = Depends(dependencies.get_current_user),
4335+
current_user: dependencies.User = Depends(dependencies.get_current_user_hybrid),
43434336
):
43444337
"""
43454338
Get status of a background job.
@@ -5935,30 +5928,28 @@ async def semantic_query(
59355928
# Execute semantic search for hybrid or degraded mode
59365929
if search_mode_actual in ["semantic", "hybrid"]:
59375930
try:
5938-
semantic_results_raw = (
5939-
semantic_query_manager.query_user_repositories(
5940-
username=current_user.username,
5941-
query_text=request.query_text,
5942-
repository_alias=request.repository_alias,
5943-
limit=request.limit,
5944-
min_score=request.min_score,
5945-
file_extensions=request.file_extensions,
5946-
# Phase 1 parameters (Story #503)
5947-
exclude_language=request.exclude_language,
5948-
exclude_path=request.exclude_path,
5949-
accuracy=request.accuracy,
5950-
# Temporal parameters (Story #446)
5951-
time_range=request.time_range,
5952-
time_range_all=request.time_range_all,
5953-
at_commit=request.at_commit,
5954-
include_removed=request.include_removed,
5955-
show_evolution=request.show_evolution,
5956-
evolution_limit=request.evolution_limit,
5957-
# Phase 3 temporal filtering parameters (Story #503)
5958-
diff_type=request.diff_type,
5959-
author=request.author,
5960-
chunk_type=request.chunk_type,
5961-
)
5931+
semantic_results_raw = semantic_query_manager.query_user_repositories(
5932+
username=current_user.username,
5933+
query_text=request.query_text,
5934+
repository_alias=request.repository_alias,
5935+
limit=request.limit,
5936+
min_score=request.min_score,
5937+
file_extensions=request.file_extensions,
5938+
# Phase 1 parameters (Story #503)
5939+
exclude_language=request.exclude_language,
5940+
exclude_path=request.exclude_path,
5941+
accuracy=request.accuracy,
5942+
# Temporal parameters (Story #446)
5943+
time_range=request.time_range,
5944+
time_range_all=request.time_range_all,
5945+
at_commit=request.at_commit,
5946+
include_removed=request.include_removed,
5947+
show_evolution=request.show_evolution,
5948+
evolution_limit=request.evolution_limit,
5949+
# Phase 3 temporal filtering parameters (Story #503)
5950+
diff_type=request.diff_type,
5951+
author=request.author,
5952+
chunk_type=request.chunk_type,
59625953
)
59635954
semantic_results_list = [
59645955
QueryResultItem(**result)

src/code_indexer/server/auth/dependencies.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,152 @@ async def get_current_user_for_mcp(request: Request) -> User:
499499
detail="Authentication required",
500500
headers={"WWW-Authenticate": _build_www_authenticate_header()},
501501
)
502+
503+
504+
async def _hybrid_auth_impl(
505+
request: Request,
506+
credentials: Optional[HTTPAuthorizationCredentials],
507+
require_admin: bool = False,
508+
) -> User:
509+
"""
510+
Internal implementation for hybrid authentication.
511+
512+
Args:
513+
request: FastAPI Request object
514+
credentials: Optional bearer token credentials
515+
require_admin: If True, require admin role
516+
517+
Returns:
518+
Authenticated User object
519+
520+
Raises:
521+
HTTPException: If authentication fails
522+
"""
523+
from code_indexer.server.web.auth import get_session_manager, SESSION_COOKIE_NAME
524+
import logging
525+
526+
logger = logging.getLogger(__name__)
527+
auth_type = "admin" if require_admin else "user"
528+
529+
# Try session-based auth first (for web UI)
530+
session_manager = get_session_manager()
531+
session_cookie_value = request.cookies.get(SESSION_COOKIE_NAME)
532+
533+
logger.info(
534+
f"Hybrid auth ({auth_type}): session_cookie={'present' if session_cookie_value else 'absent'}"
535+
)
536+
537+
if session_cookie_value:
538+
session = session_manager.get_session(request)
539+
logger.info(
540+
f"Hybrid auth ({auth_type}): session={'valid' if session else 'invalid'}, "
541+
f"username={session.username if session else None}, "
542+
f"role={session.role if session else None}"
543+
)
544+
545+
# Check admin requirement for session auth
546+
if session:
547+
if require_admin and session.role != "admin":
548+
logger.debug(f"Hybrid auth ({auth_type}): Session valid but not admin")
549+
else:
550+
# Create User object from session
551+
if not user_manager:
552+
logger.error(
553+
f"Hybrid auth ({auth_type}): user_manager not initialized"
554+
)
555+
raise HTTPException(
556+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
557+
detail="User manager not initialized",
558+
)
559+
user = user_manager.get_user(session.username)
560+
logger.debug(
561+
f"Hybrid auth ({auth_type}): user lookup for {session.username}: {user is not None}"
562+
)
563+
if user:
564+
logger.info(
565+
f"Hybrid auth ({auth_type}): Session auth SUCCESS for {session.username}"
566+
)
567+
return user
568+
# Session is valid but user not found - this shouldn't happen
569+
logger.error(
570+
f"Hybrid auth ({auth_type}): User {session.username} not found in database"
571+
)
572+
raise HTTPException(
573+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
574+
detail=f"User '{session.username}' not found in user database",
575+
)
576+
else:
577+
logger.debug(f"Hybrid auth ({auth_type}): Session invalid")
578+
579+
# Fall back to token-based auth only if no session cookie exists
580+
if not session_cookie_value and credentials:
581+
try:
582+
current_user = get_current_user(request, credentials)
583+
584+
# Check admin requirement for token auth
585+
if require_admin and not current_user.has_permission("manage_users"):
586+
raise HTTPException(
587+
status_code=status.HTTP_403_FORBIDDEN,
588+
detail="Admin access required",
589+
)
590+
591+
logger.info(
592+
f"Hybrid auth ({auth_type}): Token auth SUCCESS for {current_user.username}"
593+
)
594+
return current_user
595+
except HTTPException:
596+
raise
597+
598+
# No valid authentication found
599+
logger.warning(f"Hybrid auth ({auth_type}): No valid authentication found")
600+
raise HTTPException(
601+
status_code=status.HTTP_401_UNAUTHORIZED,
602+
detail="Authentication required",
603+
headers={"WWW-Authenticate": _build_www_authenticate_header()},
604+
)
605+
606+
607+
async def get_current_user_hybrid(
608+
request: Request,
609+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
610+
) -> User:
611+
"""
612+
Get current user supporting both session-based and token-based authentication.
613+
614+
This function tries session-based authentication first (for web UI),
615+
then falls back to token-based authentication (for API clients).
616+
617+
Args:
618+
request: FastAPI Request object
619+
credentials: Optional bearer token credentials
620+
621+
Returns:
622+
Authenticated User object
623+
624+
Raises:
625+
HTTPException: If authentication fails
626+
"""
627+
return await _hybrid_auth_impl(request, credentials, require_admin=False)
628+
629+
630+
async def get_current_admin_user_hybrid(
631+
request: Request,
632+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
633+
) -> User:
634+
"""
635+
Get current admin user supporting both session-based and token-based authentication.
636+
637+
This dependency tries session-based auth first (for web UI), then falls back to
638+
token-based auth (for API clients).
639+
640+
Args:
641+
request: FastAPI request object
642+
credentials: Optional bearer token credentials
643+
644+
Returns:
645+
User with admin role
646+
647+
Raises:
648+
HTTPException: If not authenticated or not admin
649+
"""
650+
return await _hybrid_auth_impl(request, credentials, require_admin=True)

src/code_indexer/server/jobs/manager.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def __init__(
110110
from code_indexer.server.storage.sqlite_backends import (
111111
SyncJobsSqliteBackend,
112112
)
113+
113114
self._sqlite_backend = SyncJobsSqliteBackend(db_path)
114115

115116
self._jobs: Dict[str, SyncJob] = {}
@@ -446,8 +447,14 @@ def create_job(
446447
job_id=job_id,
447448
username=username,
448449
user_alias=user_alias,
449-
job_type=job_type.value if hasattr(job_type, "value") else str(job_type),
450-
status=initial_status.value if hasattr(initial_status, "value") else str(initial_status),
450+
job_type=(
451+
job_type.value if hasattr(job_type, "value") else str(job_type)
452+
),
453+
status=(
454+
initial_status.value
455+
if hasattr(initial_status, "value")
456+
else str(initial_status)
457+
),
451458
repository_url=repository_url,
452459
)
453460

@@ -557,7 +564,11 @@ def mark_job_completed(
557564
if self._use_sqlite and self._sqlite_backend is not None:
558565
self._sqlite_backend.update_job(
559566
job_id=job_id,
560-
status=job.status.value if hasattr(job.status, "value") else str(job.status),
567+
status=(
568+
job.status.value
569+
if hasattr(job.status, "value")
570+
else str(job.status)
571+
),
561572
completed_at=completed_at.isoformat(),
562573
progress=job.progress,
563574
error_message=error_message,
@@ -619,7 +630,9 @@ def cancel_job(self, job_id: str) -> None:
619630
self._sqlite_backend.update_job(
620631
job_id=job_id,
621632
status=JobStatus.CANCELLED.value,
622-
completed_at=job.completed_at.isoformat() if job.completed_at else None,
633+
completed_at=(
634+
job.completed_at.isoformat() if job.completed_at else None
635+
),
623636
)
624637

625638
# Persist changes (JSON file, no-op for SQLite)

src/code_indexer/server/models/auto_discovery.py

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,19 @@ class DiscoveredRepository(BaseModel):
1818
platform: Literal["gitlab", "github"] = Field(
1919
..., description="Platform source (gitlab or github)"
2020
)
21-
name: str = Field(
22-
..., min_length=1, description="Full path (e.g., group/project)"
23-
)
24-
description: Optional[str] = Field(
25-
None, description="Project description"
26-
)
27-
clone_url_https: str = Field(
28-
..., description="HTTPS clone URL"
29-
)
30-
clone_url_ssh: str = Field(
31-
..., description="SSH clone URL"
32-
)
33-
default_branch: str = Field(
34-
..., description="Default branch (main/master/etc)"
35-
)
21+
name: str = Field(..., min_length=1, description="Full path (e.g., group/project)")
22+
description: Optional[str] = Field(None, description="Project description")
23+
clone_url_https: str = Field(..., description="HTTPS clone URL")
24+
clone_url_ssh: str = Field(..., description="SSH clone URL")
25+
default_branch: str = Field(..., description="Default branch (main/master/etc)")
3626
last_commit_hash: Optional[str] = Field(
3727
None, description="Short hash of last commit"
3828
)
39-
last_commit_author: Optional[str] = Field(
40-
None, description="Author of last commit"
41-
)
29+
last_commit_author: Optional[str] = Field(None, description="Author of last commit")
4230
last_activity: Optional[datetime] = Field(
4331
None, description="Last activity timestamp"
4432
)
45-
is_private: bool = Field(
46-
..., description="Whether repository is private"
47-
)
33+
is_private: bool = Field(..., description="Whether repository is private")
4834

4935
@field_validator("clone_url_https")
5036
@classmethod
@@ -72,18 +58,10 @@ class RepositoryDiscoveryResult(BaseModel):
7258
total_count: int = Field(
7359
..., ge=0, description="Total number of repositories available"
7460
)
75-
page: int = Field(
76-
..., ge=1, description="Current page number (1-indexed)"
77-
)
78-
page_size: int = Field(
79-
..., ge=1, description="Number of items per page"
80-
)
81-
total_pages: int = Field(
82-
..., ge=0, description="Total number of pages"
83-
)
84-
platform: Literal["gitlab", "github"] = Field(
85-
..., description="Platform source"
86-
)
61+
page: int = Field(..., ge=1, description="Current page number (1-indexed)")
62+
page_size: int = Field(..., ge=1, description="Number of items per page")
63+
total_pages: int = Field(..., ge=0, description="Total number of pages")
64+
platform: Literal["gitlab", "github"] = Field(..., description="Platform source")
8765

8866

8967
class DiscoveryProviderError(BaseModel):
@@ -95,9 +73,5 @@ class DiscoveryProviderError(BaseModel):
9573
error_type: Literal["not_configured", "api_error", "auth_error", "timeout"] = Field(
9674
..., description="Type of error"
9775
)
98-
message: str = Field(
99-
..., description="Human-readable error message"
100-
)
101-
details: Optional[str] = Field(
102-
None, description="Additional error details"
103-
)
76+
message: str = Field(..., description="Human-readable error message")
77+
details: Optional[str] = Field(None, description="Additional error details")

src/code_indexer/server/multi/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
from .multi_result_aggregator import MultiResultAggregator
1414
from .multi_search_service import MultiSearchService
1515
from .models import MultiSearchRequest, MultiSearchResponse, MultiSearchMetadata
16-
from .scip_models import SCIPMultiRequest, SCIPMultiResponse, SCIPMultiMetadata, SCIPResult
16+
from .scip_models import (
17+
SCIPMultiRequest,
18+
SCIPMultiResponse,
19+
SCIPMultiMetadata,
20+
SCIPResult,
21+
)
1722
from .scip_multi_service import SCIPMultiService
1823

1924
__all__ = [

0 commit comments

Comments
 (0)