Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 29 additions & 38 deletions src/code_indexer/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1931,18 +1931,7 @@ def create_app() -> FastAPI:
Returns:
Configured FastAPI app
"""
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
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

# Story #526: Initialize server-side HNSW cache at bootstrap for 1800x performance
# Import and initialize global cache instance
Expand Down Expand Up @@ -4233,9 +4222,12 @@ async def refresh_golden_repo(
status_code=202,
)
async def add_golden_repo_index(
http_request: Request,
alias: str,
request: AddIndexRequest,
current_user: dependencies.User = Depends(dependencies.get_current_admin_user),
current_user: dependencies.User = Depends(
dependencies.get_current_admin_user_hybrid
),
):
"""
Add an index type to a golden repository (admin only) - async operation.
Expand Down Expand Up @@ -4338,8 +4330,9 @@ async def get_golden_repo_index_status(

@app.get("/api/jobs/{job_id}", response_model=JobStatusResponse)
async def get_job_status(
http_request: Request,
job_id: str,
current_user: dependencies.User = Depends(dependencies.get_current_user),
current_user: dependencies.User = Depends(dependencies.get_current_user_hybrid),
):
"""
Get status of a background job.
Expand Down Expand Up @@ -5935,30 +5928,28 @@ async def semantic_query(
# Execute semantic search for hybrid or degraded mode
if search_mode_actual in ["semantic", "hybrid"]:
try:
semantic_results_raw = (
semantic_query_manager.query_user_repositories(
username=current_user.username,
query_text=request.query_text,
repository_alias=request.repository_alias,
limit=request.limit,
min_score=request.min_score,
file_extensions=request.file_extensions,
# Phase 1 parameters (Story #503)
exclude_language=request.exclude_language,
exclude_path=request.exclude_path,
accuracy=request.accuracy,
# Temporal parameters (Story #446)
time_range=request.time_range,
time_range_all=request.time_range_all,
at_commit=request.at_commit,
include_removed=request.include_removed,
show_evolution=request.show_evolution,
evolution_limit=request.evolution_limit,
# Phase 3 temporal filtering parameters (Story #503)
diff_type=request.diff_type,
author=request.author,
chunk_type=request.chunk_type,
)
semantic_results_raw = semantic_query_manager.query_user_repositories(
username=current_user.username,
query_text=request.query_text,
repository_alias=request.repository_alias,
limit=request.limit,
min_score=request.min_score,
file_extensions=request.file_extensions,
# Phase 1 parameters (Story #503)
exclude_language=request.exclude_language,
exclude_path=request.exclude_path,
accuracy=request.accuracy,
# Temporal parameters (Story #446)
time_range=request.time_range,
time_range_all=request.time_range_all,
at_commit=request.at_commit,
include_removed=request.include_removed,
show_evolution=request.show_evolution,
evolution_limit=request.evolution_limit,
# Phase 3 temporal filtering parameters (Story #503)
diff_type=request.diff_type,
author=request.author,
chunk_type=request.chunk_type,
)
semantic_results_list = [
QueryResultItem(**result)
Expand Down
149 changes: 149 additions & 0 deletions src/code_indexer/server/auth/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,152 @@ async def get_current_user_for_mcp(request: Request) -> User:
detail="Authentication required",
headers={"WWW-Authenticate": _build_www_authenticate_header()},
)


async def _hybrid_auth_impl(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials],
require_admin: bool = False,
) -> User:
"""
Internal implementation for hybrid authentication.

Args:
request: FastAPI Request object
credentials: Optional bearer token credentials
require_admin: If True, require admin role

Returns:
Authenticated User object

Raises:
HTTPException: If authentication fails
"""
from code_indexer.server.web.auth import get_session_manager, SESSION_COOKIE_NAME
import logging

logger = logging.getLogger(__name__)
auth_type = "admin" if require_admin else "user"

# Try session-based auth first (for web UI)
session_manager = get_session_manager()
session_cookie_value = request.cookies.get(SESSION_COOKIE_NAME)

logger.info(
f"Hybrid auth ({auth_type}): session_cookie={'present' if session_cookie_value else 'absent'}"
)

if session_cookie_value:
session = session_manager.get_session(request)
logger.info(
f"Hybrid auth ({auth_type}): session={'valid' if session else 'invalid'}, "
f"username={session.username if session else None}, "
f"role={session.role if session else None}"
)

# Check admin requirement for session auth
if session:
if require_admin and session.role != "admin":
logger.debug(f"Hybrid auth ({auth_type}): Session valid but not admin")
else:
# Create User object from session
if not user_manager:
logger.error(
f"Hybrid auth ({auth_type}): user_manager not initialized"
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="User manager not initialized",
)
user = user_manager.get_user(session.username)
logger.debug(
f"Hybrid auth ({auth_type}): user lookup for {session.username}: {user is not None}"
)
if user:
logger.info(
f"Hybrid auth ({auth_type}): Session auth SUCCESS for {session.username}"
)
return user
# Session is valid but user not found - this shouldn't happen
logger.error(
f"Hybrid auth ({auth_type}): User {session.username} not found in database"
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"User '{session.username}' not found in user database",
)
else:
logger.debug(f"Hybrid auth ({auth_type}): Session invalid")

# Fall back to token-based auth only if no session cookie exists
if not session_cookie_value and credentials:
try:
current_user = get_current_user(request, credentials)

# Check admin requirement for token auth
if require_admin and not current_user.has_permission("manage_users"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required",
)

logger.info(
f"Hybrid auth ({auth_type}): Token auth SUCCESS for {current_user.username}"
)
return current_user
except HTTPException:
raise

# No valid authentication found
logger.warning(f"Hybrid auth ({auth_type}): No valid authentication found")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required",
headers={"WWW-Authenticate": _build_www_authenticate_header()},
)


async def get_current_user_hybrid(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
) -> User:
"""
Get current user supporting both session-based and token-based authentication.

This function tries session-based authentication first (for web UI),
then falls back to token-based authentication (for API clients).

Args:
request: FastAPI Request object
credentials: Optional bearer token credentials

Returns:
Authenticated User object

Raises:
HTTPException: If authentication fails
"""
return await _hybrid_auth_impl(request, credentials, require_admin=False)


async def get_current_admin_user_hybrid(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
) -> User:
"""
Get current admin user supporting both session-based and token-based authentication.

This dependency tries session-based auth first (for web UI), then falls back to
token-based auth (for API clients).

Args:
request: FastAPI request object
credentials: Optional bearer token credentials

Returns:
User with admin role

Raises:
HTTPException: If not authenticated or not admin
"""
return await _hybrid_auth_impl(request, credentials, require_admin=True)
21 changes: 17 additions & 4 deletions src/code_indexer/server/jobs/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def __init__(
from code_indexer.server.storage.sqlite_backends import (
SyncJobsSqliteBackend,
)

self._sqlite_backend = SyncJobsSqliteBackend(db_path)

self._jobs: Dict[str, SyncJob] = {}
Expand Down Expand Up @@ -446,8 +447,14 @@ def create_job(
job_id=job_id,
username=username,
user_alias=user_alias,
job_type=job_type.value if hasattr(job_type, "value") else str(job_type),
status=initial_status.value if hasattr(initial_status, "value") else str(initial_status),
job_type=(
job_type.value if hasattr(job_type, "value") else str(job_type)
),
status=(
initial_status.value
if hasattr(initial_status, "value")
else str(initial_status)
),
repository_url=repository_url,
)

Expand Down Expand Up @@ -557,7 +564,11 @@ def mark_job_completed(
if self._use_sqlite and self._sqlite_backend is not None:
self._sqlite_backend.update_job(
job_id=job_id,
status=job.status.value if hasattr(job.status, "value") else str(job.status),
status=(
job.status.value
if hasattr(job.status, "value")
else str(job.status)
),
completed_at=completed_at.isoformat(),
progress=job.progress,
error_message=error_message,
Expand Down Expand Up @@ -619,7 +630,9 @@ def cancel_job(self, job_id: str) -> None:
self._sqlite_backend.update_job(
job_id=job_id,
status=JobStatus.CANCELLED.value,
completed_at=job.completed_at.isoformat() if job.completed_at else None,
completed_at=(
job.completed_at.isoformat() if job.completed_at else None
),
)

# Persist changes (JSON file, no-op for SQLite)
Expand Down
52 changes: 13 additions & 39 deletions src/code_indexer/server/models/auto_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,19 @@ class DiscoveredRepository(BaseModel):
platform: Literal["gitlab", "github"] = Field(
..., description="Platform source (gitlab or github)"
)
name: str = Field(
..., min_length=1, description="Full path (e.g., group/project)"
)
description: Optional[str] = Field(
None, description="Project description"
)
clone_url_https: str = Field(
..., description="HTTPS clone URL"
)
clone_url_ssh: str = Field(
..., description="SSH clone URL"
)
default_branch: str = Field(
..., description="Default branch (main/master/etc)"
)
name: str = Field(..., min_length=1, description="Full path (e.g., group/project)")
description: Optional[str] = Field(None, description="Project description")
clone_url_https: str = Field(..., description="HTTPS clone URL")
clone_url_ssh: str = Field(..., description="SSH clone URL")
default_branch: str = Field(..., description="Default branch (main/master/etc)")
last_commit_hash: Optional[str] = Field(
None, description="Short hash of last commit"
)
last_commit_author: Optional[str] = Field(
None, description="Author of last commit"
)
last_commit_author: Optional[str] = Field(None, description="Author of last commit")
last_activity: Optional[datetime] = Field(
None, description="Last activity timestamp"
)
is_private: bool = Field(
..., description="Whether repository is private"
)
is_private: bool = Field(..., description="Whether repository is private")

@field_validator("clone_url_https")
@classmethod
Expand Down Expand Up @@ -72,18 +58,10 @@ class RepositoryDiscoveryResult(BaseModel):
total_count: int = Field(
..., ge=0, description="Total number of repositories available"
)
page: int = Field(
..., ge=1, description="Current page number (1-indexed)"
)
page_size: int = Field(
..., ge=1, description="Number of items per page"
)
total_pages: int = Field(
..., ge=0, description="Total number of pages"
)
platform: Literal["gitlab", "github"] = Field(
..., description="Platform source"
)
page: int = Field(..., ge=1, description="Current page number (1-indexed)")
page_size: int = Field(..., ge=1, description="Number of items per page")
total_pages: int = Field(..., ge=0, description="Total number of pages")
platform: Literal["gitlab", "github"] = Field(..., description="Platform source")


class DiscoveryProviderError(BaseModel):
Expand All @@ -95,9 +73,5 @@ class DiscoveryProviderError(BaseModel):
error_type: Literal["not_configured", "api_error", "auth_error", "timeout"] = Field(
..., description="Type of error"
)
message: str = Field(
..., description="Human-readable error message"
)
details: Optional[str] = Field(
None, description="Additional error details"
)
message: str = Field(..., description="Human-readable error message")
details: Optional[str] = Field(None, description="Additional error details")
7 changes: 6 additions & 1 deletion src/code_indexer/server/multi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
from .multi_result_aggregator import MultiResultAggregator
from .multi_search_service import MultiSearchService
from .models import MultiSearchRequest, MultiSearchResponse, MultiSearchMetadata
from .scip_models import SCIPMultiRequest, SCIPMultiResponse, SCIPMultiMetadata, SCIPResult
from .scip_models import (
SCIPMultiRequest,
SCIPMultiResponse,
SCIPMultiMetadata,
SCIPResult,
)
from .scip_multi_service import SCIPMultiService

__all__ = [
Expand Down
Loading
Loading