feat: implement permission-aware search baseline
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.core.search import SearchQuery
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_search.backend.manifest import READ_SCOPE
|
||||
from govoplan_search.backend.schemas import (
|
||||
SearchProviderListResponse,
|
||||
SearchProviderResponse,
|
||||
SearchResponse,
|
||||
SearchResultResponse,
|
||||
)
|
||||
from govoplan_search.backend.service import aggregate_search
|
||||
|
||||
|
||||
router = APIRouter(prefix="/search", tags=["search"])
|
||||
|
||||
|
||||
def _registry(request: Request) -> PlatformRegistry:
|
||||
registry = getattr(request.app.state, "govoplan_registry", None)
|
||||
if not isinstance(registry, PlatformRegistry):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Search registry is not available.",
|
||||
)
|
||||
return registry
|
||||
|
||||
|
||||
def _require_read(principal: ApiPrincipal) -> None:
|
||||
if not has_scope(principal, READ_SCOPE):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Missing scope: {READ_SCOPE}",
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=SearchResponse)
|
||||
def api_search(
|
||||
request: Request,
|
||||
q: str = Query(default="", max_length=500),
|
||||
module: list[str] = Query(default=[]),
|
||||
resource_type: list[str] = Query(default=[]),
|
||||
context_kind: str = Query(default="global", pattern="^(global|module|resource)$"),
|
||||
context_id: str | None = Query(default=None, max_length=255),
|
||||
limit: int = Query(default=25, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0, le=10_000),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> SearchResponse:
|
||||
_require_read(principal)
|
||||
registry = _registry(request)
|
||||
query = SearchQuery(
|
||||
text=q,
|
||||
tenant_id=principal.tenant_id,
|
||||
module_ids=tuple(dict.fromkeys(module)),
|
||||
resource_types=tuple(dict.fromkeys(resource_type)),
|
||||
context_kind=context_kind, # type: ignore[arg-type]
|
||||
context_id=context_id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
results, diagnostics = aggregate_search(
|
||||
registry,
|
||||
session,
|
||||
principal,
|
||||
query=query,
|
||||
)
|
||||
return SearchResponse(
|
||||
query=query.text,
|
||||
results=[
|
||||
SearchResultResponse.model_validate(
|
||||
{
|
||||
"provider_id": result.provider_id,
|
||||
"module_id": result.module_id,
|
||||
"resource_type": result.resource_type,
|
||||
"resource_id": result.resource_id,
|
||||
"title": result.title,
|
||||
"summary": result.summary,
|
||||
"url": result.url,
|
||||
"score": result.score,
|
||||
"highlights": list(result.highlights),
|
||||
"breadcrumbs": list(result.breadcrumbs),
|
||||
"external_reference": (
|
||||
result.external_reference.to_dict()
|
||||
if result.external_reference is not None
|
||||
else None
|
||||
),
|
||||
"metadata": dict(result.metadata),
|
||||
}
|
||||
)
|
||||
for result in results
|
||||
],
|
||||
diagnostics=list(diagnostics),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/providers", response_model=SearchProviderListResponse)
|
||||
def api_search_providers(
|
||||
request: Request,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> SearchProviderListResponse:
|
||||
_require_read(principal)
|
||||
registrations = _registry(request).search_provider_registrations()
|
||||
return SearchProviderListResponse(
|
||||
providers=[
|
||||
SearchProviderResponse(
|
||||
id=item.registration.id,
|
||||
module_id=item.module_id,
|
||||
resource_types=list(item.registration.resource_types),
|
||||
order=item.registration.order,
|
||||
)
|
||||
for item in registrations
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["router"]
|
||||
Reference in New Issue
Block a user