sprint2: 8 services + 8 routes visibility filter + BaseSearchProvider + owned_mixin on models
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""Base search provider with visibility filter — all search providers should inherit from this.
|
||||
|
||||
This ensures that search results respect row-level security automatically.
|
||||
Plugins that provide search functionality should use this base class.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseSearchProvider:
|
||||
"""Base class for search providers with built-in visibility filtering.
|
||||
|
||||
Subclasses must implement _search_fts_filtered() and _search_vector_filtered().
|
||||
The base class handles loading visible IDs and passing them to the subclass.
|
||||
"""
|
||||
|
||||
entity_type: str = "" # Override in subclass
|
||||
|
||||
async def search_fts(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tsquery: str,
|
||||
tenant_id: uuid.UUID,
|
||||
limit: int,
|
||||
user_id: uuid.UUID | None = None,
|
||||
is_system_admin: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Full-text search with visibility filter."""
|
||||
if is_system_admin or not user_id:
|
||||
return await self._search_fts_filtered(db, tsquery, tenant_id, limit, None)
|
||||
|
||||
visible_ids = await self._get_visible_ids(db, tenant_id, user_id)
|
||||
if not visible_ids:
|
||||
return []
|
||||
return await self._search_fts_filtered(db, tsquery, tenant_id, limit, visible_ids)
|
||||
|
||||
async def search_vector(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
embedding: list[float],
|
||||
tenant_id: uuid.UUID,
|
||||
limit: int,
|
||||
user_id: uuid.UUID | None = None,
|
||||
is_system_admin: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Semantic vector search with visibility filter."""
|
||||
if is_system_admin or not user_id:
|
||||
return await self._search_vector_filtered(db, embedding, tenant_id, limit, None)
|
||||
|
||||
visible_ids = await self._get_visible_ids(db, tenant_id, user_id)
|
||||
if not visible_ids:
|
||||
return []
|
||||
return await self._search_vector_filtered(db, embedding, tenant_id, limit, visible_ids)
|
||||
|
||||
async def _get_visible_ids(
|
||||
self, db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID
|
||||
) -> set[uuid.UUID] | None:
|
||||
"""Get visible entity IDs for the user."""
|
||||
from app.services.entity_permission_service import get_visible_ids
|
||||
visible, _ = await get_visible_ids(db, tenant_id, user_id, self.entity_type)
|
||||
return visible if visible else None
|
||||
|
||||
async def _search_fts_filtered(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tsquery: str,
|
||||
tenant_id: uuid.UUID,
|
||||
limit: int,
|
||||
visible_ids: set[uuid.UUID] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Override: FTS search filtered by visible_ids. If visible_ids is None, no filter."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def _search_vector_filtered(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
embedding: list[float],
|
||||
tenant_id: uuid.UUID,
|
||||
limit: int,
|
||||
visible_ids: set[uuid.UUID] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Override: Vector search filtered by visible_ids. If visible_ids is None, no filter."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_embedding_text(
|
||||
self, db: AsyncSession, entity_id: uuid.UUID, tenant_id: uuid.UUID
|
||||
) -> str:
|
||||
"""Override: Get text for embedding generation."""
|
||||
raise NotImplementedError
|
||||
Reference in New Issue
Block a user