From 493e8d5a64624bfd861617f307d82dfbeb9abdfe Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:27:47 +0530 Subject: [PATCH] feat: enforce API access for knowledge resources --- surfsense_backend/app/file_storage/api.py | 11 +- .../app/routes/documents_routes.py | 102 +++++++++++------- surfsense_backend/app/routes/editor_routes.py | 23 ++-- .../app/routes/folders_routes.py | 58 ++++++---- surfsense_backend/app/routes/notes_routes.py | 18 ++-- .../app/routes/reports_routes.py | 31 ++++-- .../routes/search_source_connectors_routes.py | 75 ++++++++----- .../app/routes/team_memory_routes.py | 18 ++-- 8 files changed, 206 insertions(+), 130 deletions(-) diff --git a/surfsense_backend/app/file_storage/api.py b/surfsense_backend/app/file_storage/api.py index c649ba63d..fd08a6244 100644 --- a/surfsense_backend/app/file_storage/api.py +++ b/surfsense_backend/app/file_storage/api.py @@ -9,6 +9,7 @@ from fastapi.responses import StreamingResponse from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from app.auth.context import AuthContext from app.db import Document, Permission, User, get_async_session from app.file_storage.persistence.enums import DocumentFileKind from app.file_storage.schemas import DocumentFileRead @@ -17,7 +18,7 @@ from app.file_storage.service import ( list_document_files, open_document_file_stream, ) -from app.users import current_active_user +from app.users import get_auth_context from app.utils.rbac import check_permission router = APIRouter() @@ -35,7 +36,7 @@ async def _load_readable_document( await check_permission( session, - user, + auth, document.search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -57,8 +58,9 @@ def _content_disposition(filename: str) -> str: async def read_document_files( document_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ) -> list[DocumentFileRead]: + user = auth.user """Return metadata for every stored file of a document (gates the UI).""" await _load_readable_document(document_id=document_id, session=session, user=user) records = await list_document_files(session, document_id=document_id) @@ -69,8 +71,9 @@ async def read_document_files( async def download_original_document_file( document_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ) -> StreamingResponse: + user = auth.user """Stream the document's original uploaded file.""" await _load_readable_document(document_id=document_id, session=session, user=user) diff --git a/surfsense_backend/app/routes/documents_routes.py b/surfsense_backend/app/routes/documents_routes.py index 53f03a0ca..3991af445 100644 --- a/surfsense_backend/app/routes/documents_routes.py +++ b/surfsense_backend/app/routes/documents_routes.py @@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from sqlalchemy.orm import selectinload +from app.auth.context import AuthContext from app.agents.chat.runtime.path_resolver import virtual_path_to_doc from app.db import ( Chunk, @@ -35,7 +36,7 @@ from app.schemas import ( PaginatedResponse, ) from app.services.task_dispatcher import TaskDispatcher, get_task_dispatcher -from app.users import current_active_user +from app.users import get_auth_context from app.utils.rbac import check_permission try: @@ -60,8 +61,9 @@ MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024 # 500 MB per file async def create_documents( request: DocumentsCreate, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Create new documents. Requires DOCUMENTS_CREATE permission. @@ -70,7 +72,7 @@ async def create_documents( # Check permission await check_permission( session, - user, + auth, request.search_space_id, Permission.DOCUMENTS_CREATE.value, "You don't have permission to create documents in this search space", @@ -128,9 +130,10 @@ async def create_documents_file_upload( use_vision_llm: bool = Form(False), processing_mode: str = Form("basic"), session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), dispatcher: TaskDispatcher = Depends(get_task_dispatcher), ): + user = auth.user """ Upload files as documents with real-time status tracking. @@ -159,7 +162,7 @@ async def create_documents_file_upload( try: await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_CREATE.value, "You don't have permission to create documents in this search space", @@ -340,8 +343,9 @@ async def read_documents( sort_by: str = "created_at", sort_order: str = "desc", session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ List documents the user has access to, with optional filtering and pagination. Requires DOCUMENTS_READ permission for the search space(s). @@ -369,7 +373,7 @@ async def read_documents( if search_space_id is not None: await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -519,8 +523,9 @@ async def search_documents( search_space_id: int | None = None, document_types: str | None = None, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Search documents by title substring, optionally filtered by search_space_id and document_types. Requires DOCUMENTS_READ permission for the search space(s). @@ -549,7 +554,7 @@ async def search_documents( if search_space_id is not None: await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -677,8 +682,9 @@ async def search_document_titles( page: int = 0, page_size: int = 20, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Lightweight document title search optimized for mention picker (@mentions). @@ -703,7 +709,7 @@ async def search_document_titles( # Check permission for the search space await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -781,8 +787,9 @@ async def get_document_by_virtual_path( search_space_id: int, virtual_path: str, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Resolve a knowledge-base document by its agent-facing virtual path. The agent renders every document under ``/documents/...`` with a @@ -804,7 +811,7 @@ async def get_document_by_virtual_path( try: await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -838,8 +845,9 @@ async def get_documents_status( search_space_id: int, document_ids: str, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Batch status endpoint for documents in a search space. @@ -849,7 +857,7 @@ async def get_documents_status( try: await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -905,8 +913,9 @@ async def get_documents_status( async def get_document_type_counts( search_space_id: int | None = None, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Get counts of documents by type for search spaces the user has access to. Requires DOCUMENTS_READ permission for the search space(s). @@ -926,7 +935,7 @@ async def get_document_type_counts( # Check permission for specific search space await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -965,8 +974,9 @@ async def get_document_by_chunk_id( 5, ge=0, description="Number of chunks before/after the cited chunk to include" ), session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Retrieves a document based on a chunk ID, including a window of chunks around the cited one. Uses SQL-level pagination to avoid loading all chunks into memory. @@ -995,7 +1005,7 @@ async def get_document_by_chunk_id( await check_permission( session, - user, + auth, document.search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -1060,12 +1070,13 @@ async def get_document_by_chunk_id( async def get_watched_folders( search_space_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Return root folders that are marked as watched (metadata->>'watched' = 'true').""" await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -1101,8 +1112,9 @@ async def get_document_chunks_paginated( None, ge=0, description="Direct offset; overrides page * page_size" ), session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Paginated chunk loading for a document. Supports both page-based and offset-based access. @@ -1120,7 +1132,7 @@ async def get_document_chunks_paginated( await check_permission( session, - user, + auth, document.search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -1162,8 +1174,9 @@ async def get_document_chunks_paginated( async def read_document( document_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Get a specific document by ID. Requires DOCUMENTS_READ permission for the search space. @@ -1182,7 +1195,7 @@ async def read_document( # Check permission for the search space await check_permission( session, - user, + auth, document.search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -1216,8 +1229,9 @@ async def update_document( document_id: int, document_update: DocumentUpdate, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Update a document. Requires DOCUMENTS_UPDATE permission for the search space. @@ -1236,7 +1250,7 @@ async def update_document( # Check permission for the search space await check_permission( session, - user, + auth, db_document.search_space_id, Permission.DOCUMENTS_UPDATE.value, "You don't have permission to update documents in this search space", @@ -1275,8 +1289,9 @@ async def update_document( async def delete_document( document_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Delete a document. Requires DOCUMENTS_DELETE permission for the search space. @@ -1311,7 +1326,7 @@ async def delete_document( # Check permission for the search space await check_permission( session, - user, + auth, document.search_space_id, Permission.DOCUMENTS_DELETE.value, "You don't have permission to delete documents in this search space", @@ -1355,8 +1370,9 @@ async def delete_document( async def list_document_versions( document_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """List all versions for a document, ordered by version_number descending.""" document = ( await session.execute(select(Document).where(Document.id == document_id)) @@ -1396,8 +1412,9 @@ async def get_document_version( document_id: int, version_number: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Get full version content including source_markdown.""" document = ( await session.execute(select(Document).where(Document.id == document_id)) @@ -1434,8 +1451,9 @@ async def restore_document_version( document_id: int, version_number: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Restore a previous version: snapshot current state, then overwrite document content.""" document = ( await session.execute(select(Document).where(Document.id == document_id)) @@ -1517,8 +1535,9 @@ class FolderSyncFinalizeRequest(PydanticBaseModel): async def folder_mtime_check( request: FolderMtimeCheckRequest, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Pre-upload optimization: check which files need uploading based on mtime. Returns the subset of relative paths where the file is new or has a @@ -1528,7 +1547,7 @@ async def folder_mtime_check( await check_permission( session, - user, + auth, request.search_space_id, Permission.DOCUMENTS_CREATE.value, "You don't have permission to create documents in this search space", @@ -1587,8 +1606,9 @@ async def folder_upload( use_vision_llm: bool = Form(False), processing_mode: str = Form("basic"), session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Upload files from the desktop app for folder indexing. Files are written to temp storage and dispatched to a Celery task. @@ -1603,7 +1623,7 @@ async def folder_upload( await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_CREATE.value, "You don't have permission to create documents in this search space", @@ -1733,8 +1753,9 @@ async def folder_upload( async def folder_unlink( request: FolderUnlinkRequest, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Handle file deletion events from the desktop watcher. For each relative path, find the matching document and delete it. @@ -1746,7 +1767,7 @@ async def folder_unlink( await check_permission( session, - user, + auth, request.search_space_id, Permission.DOCUMENTS_DELETE.value, "You don't have permission to delete documents in this search space", @@ -1787,8 +1808,9 @@ async def folder_unlink( async def folder_sync_finalize( request: FolderSyncFinalizeRequest, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Finalize a full folder scan by deleting orphaned documents. The client sends the complete list of relative paths currently in the @@ -1803,7 +1825,7 @@ async def folder_sync_finalize( await check_permission( session, - user, + auth, request.search_space_id, Permission.DOCUMENTS_DELETE.value, "You don't have permission to delete documents in this search space", diff --git a/surfsense_backend/app/routes/editor_routes.py b/surfsense_backend/app/routes/editor_routes.py index 8250fff98..fe00995ea 100644 --- a/surfsense_backend/app/routes/editor_routes.py +++ b/surfsense_backend/app/routes/editor_routes.py @@ -18,6 +18,7 @@ from fastapi.responses import StreamingResponse from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession +from app.auth.context import AuthContext from app.db import Chunk, Document, DocumentType, Permission, User, get_async_session from app.routes.reports_routes import ( _FILE_EXTENSIONS, @@ -31,7 +32,7 @@ from app.templates.export_helpers import ( get_reference_docx_path, get_typst_template_path, ) -from app.users import current_active_user +from app.users import get_auth_context from app.utils.rbac import check_permission logger = logging.getLogger(__name__) @@ -47,8 +48,9 @@ async def get_editor_content( search_space_id: int, document_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Get document content for editing. @@ -60,7 +62,7 @@ async def get_editor_content( # Check RBAC permission await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -178,15 +180,16 @@ async def download_document_markdown( search_space_id: int, document_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Download the full document content as a .md file. Reconstructs markdown from source_markdown or chunks. """ await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", @@ -244,8 +247,9 @@ async def save_document( document_id: int, data: dict[str, Any], session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Save document markdown and trigger reindexing. Called when user clicks 'Save & Exit'. @@ -259,7 +263,7 @@ async def save_document( # Check RBAC permission await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_UPDATE.value, "You don't have permission to update documents in this search space", @@ -331,12 +335,13 @@ async def export_document( description="Export format: pdf, docx, html, latex, epub, odt, or plain", ), session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Export a document in the requested format (reuses the report export pipeline).""" await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read documents in this search space", diff --git a/surfsense_backend/app/routes/folders_routes.py b/surfsense_backend/app/routes/folders_routes.py index dca55f31e..8a5dfcb73 100644 --- a/surfsense_backend/app/routes/folders_routes.py +++ b/surfsense_backend/app/routes/folders_routes.py @@ -5,6 +5,7 @@ from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select +from app.auth.context import AuthContext from app.db import Document, Folder, Permission, User, get_async_session from app.schemas import ( BulkDocumentMove, @@ -23,7 +24,7 @@ from app.services.folder_service import ( get_subtree_max_depth, validate_folder_depth, ) -from app.users import current_active_user +from app.users import get_auth_context from app.utils.rbac import check_permission router = APIRouter() @@ -33,13 +34,14 @@ router = APIRouter() async def create_folder( request: FolderCreate, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Create a new folder. Requires DOCUMENTS_CREATE permission.""" try: await check_permission( session, - user, + auth, request.search_space_id, Permission.DOCUMENTS_CREATE.value, "You don't have permission to create folders in this search space", @@ -91,13 +93,14 @@ async def create_folder( async def list_folders( search_space_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """List all folders in a search space (flat). Requires DOCUMENTS_READ permission.""" try: await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read folders in this search space", @@ -122,8 +125,9 @@ async def list_folders( async def get_folder( folder_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Get a single folder. Requires DOCUMENTS_READ permission.""" try: folder = await session.get(Folder, folder_id) @@ -132,7 +136,7 @@ async def get_folder( await check_permission( session, - user, + auth, folder.search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read folders in this search space", @@ -152,8 +156,9 @@ async def get_folder( async def get_folder_breadcrumb( folder_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Get ancestor chain for breadcrumb display. Requires DOCUMENTS_READ permission.""" try: folder = await session.get(Folder, folder_id) @@ -162,7 +167,7 @@ async def get_folder_breadcrumb( await check_permission( session, - user, + auth, folder.search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read folders in this search space", @@ -196,8 +201,9 @@ async def get_folder_breadcrumb( async def stop_watching_folder( folder_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Clear the watched flag from a folder's metadata.""" folder = await session.get(Folder, folder_id) if not folder: @@ -205,7 +211,7 @@ async def stop_watching_folder( await check_permission( session, - user, + auth, folder.search_space_id, Permission.DOCUMENTS_UPDATE.value, "You don't have permission to update folders in this search space", @@ -224,8 +230,9 @@ async def update_folder( folder_id: int, request: FolderUpdate, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Rename a folder. Requires DOCUMENTS_UPDATE permission.""" try: folder = await session.get(Folder, folder_id) @@ -234,7 +241,7 @@ async def update_folder( await check_permission( session, - user, + auth, folder.search_space_id, Permission.DOCUMENTS_UPDATE.value, "You don't have permission to update folders in this search space", @@ -264,8 +271,9 @@ async def move_folder( folder_id: int, request: FolderMove, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Move a folder to a new parent. Requires DOCUMENTS_UPDATE permission.""" try: folder = await session.get(Folder, folder_id) @@ -274,7 +282,7 @@ async def move_folder( await check_permission( session, - user, + auth, folder.search_space_id, Permission.DOCUMENTS_UPDATE.value, "You don't have permission to move folders in this search space", @@ -324,8 +332,9 @@ async def reorder_folder( folder_id: int, request: FolderReorder, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Reorder a folder among its siblings via fractional indexing. Requires DOCUMENTS_UPDATE.""" try: folder = await session.get(Folder, folder_id) @@ -334,7 +343,7 @@ async def reorder_folder( await check_permission( session, - user, + auth, folder.search_space_id, Permission.DOCUMENTS_UPDATE.value, "You don't have permission to reorder folders in this search space", @@ -365,8 +374,9 @@ async def reorder_folder( async def delete_folder( folder_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Mark documents for deletion and dispatch Celery to delete docs first, then folders.""" try: folder = await session.get(Folder, folder_id) @@ -375,7 +385,7 @@ async def delete_folder( await check_permission( session, - user, + auth, folder.search_space_id, Permission.DOCUMENTS_DELETE.value, "You don't have permission to delete folders in this search space", @@ -439,8 +449,9 @@ async def move_document( document_id: int, request: DocumentMove, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Move a document to a folder (or root). Requires DOCUMENTS_UPDATE permission.""" try: result = await session.execute( @@ -452,7 +463,7 @@ async def move_document( await check_permission( session, - user, + auth, document.search_space_id, Permission.DOCUMENTS_UPDATE.value, "You don't have permission to move documents in this search space", @@ -485,8 +496,9 @@ async def move_document( async def bulk_move_documents( request: BulkDocumentMove, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Move multiple documents to a folder (or root). Requires DOCUMENTS_UPDATE permission.""" try: if not request.document_ids: @@ -504,7 +516,7 @@ async def bulk_move_documents( for ss_id in search_space_ids: await check_permission( session, - user, + auth, ss_id, Permission.DOCUMENTS_UPDATE.value, "You don't have permission to move documents in this search space", diff --git a/surfsense_backend/app/routes/notes_routes.py b/surfsense_backend/app/routes/notes_routes.py index 76518de08..e5cca8700 100644 --- a/surfsense_backend/app/routes/notes_routes.py +++ b/surfsense_backend/app/routes/notes_routes.py @@ -9,9 +9,10 @@ from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from app.auth.context import AuthContext from app.db import Document, DocumentType, Permission, User, get_async_session from app.schemas import DocumentRead, PaginatedResponse -from app.users import current_active_user +from app.users import get_auth_context from app.utils.rbac import check_permission router = APIRouter() @@ -27,8 +28,9 @@ async def create_note( search_space_id: int, request: CreateNoteRequest, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Create a new note document. @@ -37,7 +39,7 @@ async def create_note( # Check RBAC permission await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_CREATE.value, "You don't have permission to create notes in this search space", @@ -98,8 +100,9 @@ async def list_notes( page: int | None = None, page_size: int = 50, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ List all notes in a search space. @@ -108,7 +111,7 @@ async def list_notes( # Check RBAC permission await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read notes in this search space", @@ -191,8 +194,9 @@ async def delete_note( search_space_id: int, note_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Delete a note. @@ -201,7 +205,7 @@ async def delete_note( # Check RBAC permission await check_permission( session, - user, + auth, search_space_id, Permission.DOCUMENTS_DELETE.value, "You don't have permission to delete notes in this search space", diff --git a/surfsense_backend/app/routes/reports_routes.py b/surfsense_backend/app/routes/reports_routes.py index 19961e1a9..d5996485e 100644 --- a/surfsense_backend/app/routes/reports_routes.py +++ b/surfsense_backend/app/routes/reports_routes.py @@ -28,6 +28,7 @@ from sqlalchemy import select from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession +from app.auth.context import AuthContext from app.db import ( Report, SearchSpace, @@ -42,7 +43,7 @@ from app.templates.export_helpers import ( get_reference_docx_path, get_typst_template_path, ) -from app.users import current_active_user +from app.users import get_auth_context from app.utils.rbac import check_search_space_access logger = logging.getLogger(__name__) @@ -158,8 +159,9 @@ def _normalize_latex_delimiters(text: str) -> str: async def _get_report_with_access( report_id: int, session: AsyncSession, - user: User, + auth: AuthContext, ) -> Report: + user = auth.user """Fetch a report and verify the user belongs to its search space. Raises HTTPException(404) if not found, HTTPException(403) if no access. @@ -172,7 +174,7 @@ async def _get_report_with_access( # Lightweight membership check - no granular RBAC, just "is the user a # member of the search space this report belongs to?" - await check_search_space_access(session, user, report.search_space_id) + await check_search_space_access(session, auth, report.search_space_id) return report @@ -206,8 +208,9 @@ async def read_reports( limit: int = Query(default=100, ge=1, le=MAX_REPORT_LIST_LIMIT), search_space_id: int | None = None, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ List reports the user has access to. Filters by search space membership. @@ -215,7 +218,7 @@ async def read_reports( try: if search_space_id is not None: # Verify the caller is a member of the requested search space - await check_search_space_access(session, user, search_space_id) + await check_search_space_access(session, auth, search_space_id) result = await session.execute( select(Report) @@ -247,8 +250,9 @@ async def read_reports( async def read_report( report_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Get a specific report by ID (metadata only, no content). """ @@ -266,8 +270,9 @@ async def read_report( async def read_report_content( report_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Get full Markdown content of a report, including version siblings. """ @@ -298,8 +303,9 @@ async def update_report_content( report_id: int, body: ReportContentUpdate, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Update the Markdown content of a report. @@ -339,8 +345,9 @@ async def update_report_content( async def preview_report_pdf( report_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Return a compiled PDF preview for Typst-based reports (resumes). @@ -394,8 +401,9 @@ async def export_report( description="Export format: pdf, docx, html, latex, epub, odt, or plain", ), session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Export a report in the requested format. """ @@ -568,8 +576,9 @@ async def export_report( async def delete_report( report_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Delete a report. """ diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py index 512b52ae4..fab79ab49 100644 --- a/surfsense_backend/app/routes/search_source_connectors_routes.py +++ b/surfsense_backend/app/routes/search_source_connectors_routes.py @@ -33,6 +33,7 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select +from app.auth.context import AuthContext from app.config import config from app.connectors.github_connector import GitHubConnector from app.db import ( @@ -56,7 +57,7 @@ from app.schemas import ( SearchSourceConnectorUpdate, ) from app.services.composio_service import ComposioService, get_composio_service -from app.users import current_active_user +from app.users import get_auth_context # NOTE: connector indexer functions are imported lazily inside each # ``run_*_indexing`` helper to break a circular import cycle: @@ -143,8 +144,9 @@ class GitHubPATRequest(BaseModel): @router.post("/github/repositories", response_model=list[dict[str, Any]]) async def list_github_repositories( pat_request: GitHubPATRequest, - user: User = Depends(current_active_user), # Ensure the user is logged in + auth: AuthContext = Depends(get_auth_context), # Ensure the user is logged in ): + user = auth.user """ Fetches a list of repositories accessible by the provided GitHub PAT. The PAT is used for this request only and is not stored. @@ -173,8 +175,9 @@ async def create_search_source_connector( ..., description="ID of the search space to associate the connector with" ), session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Create a new search source connector. Requires CONNECTORS_CREATE permission. @@ -186,7 +189,7 @@ async def create_search_source_connector( # Check if user has permission to create connectors await check_permission( session, - user, + auth, search_space_id, Permission.CONNECTORS_CREATE.value, "You don't have permission to create connectors in this search space", @@ -281,8 +284,9 @@ async def read_search_source_connectors( limit: int = 100, search_space_id: int | None = None, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ List all search source connectors for a search space. Requires CONNECTORS_READ permission. @@ -297,7 +301,7 @@ async def read_search_source_connectors( # Check if user has permission to read connectors await check_permission( session, - user, + auth, search_space_id, Permission.CONNECTORS_READ.value, "You don't have permission to view connectors in this search space", @@ -324,8 +328,9 @@ async def read_search_source_connectors( async def read_search_source_connector( connector_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Get a specific search source connector by ID. Requires CONNECTORS_READ permission. @@ -345,7 +350,7 @@ async def read_search_source_connector( # Check permission await check_permission( session, - user, + auth, connector.search_space_id, Permission.CONNECTORS_READ.value, "You don't have permission to view this connector", @@ -367,8 +372,9 @@ async def update_search_source_connector( connector_id: int, connector_update: SearchSourceConnectorUpdate, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Update a search source connector. Requires CONNECTORS_UPDATE permission. @@ -386,7 +392,7 @@ async def update_search_source_connector( # Check permission await check_permission( session, - user, + auth, db_connector.search_space_id, Permission.CONNECTORS_UPDATE.value, "You don't have permission to update this connector", @@ -557,8 +563,9 @@ async def update_search_source_connector( async def delete_search_source_connector( connector_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Delete a search source connector and all its associated documents. @@ -588,7 +595,7 @@ async def delete_search_source_connector( # Check permission await check_permission( session, - user, + auth, db_connector.search_space_id, Permission.CONNECTORS_DELETE.value, "You don't have permission to delete this connector", @@ -725,8 +732,9 @@ async def index_connector_content( description="[Google Drive only] Structured request with folders and files to index", ), session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Index content from a KB connector to a search space. @@ -760,7 +768,7 @@ async def index_connector_content( # the read/update/delete handlers — not the client-supplied query param. await check_permission( session, - user, + auth, connector.search_space_id, Permission.CONNECTORS_UPDATE.value, "You don't have permission to index content in this search space", @@ -2645,8 +2653,9 @@ async def create_mcp_connector( connector_data: MCPConnectorCreate, search_space_id: int = Query(..., description="Search space ID"), session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Create a new MCP (Model Context Protocol) connector. @@ -2669,7 +2678,7 @@ async def create_mcp_connector( # Check user has permission to create connectors await check_permission( session, - user, + auth, search_space_id, Permission.CONNECTORS_CREATE.value, "You don't have permission to create connectors in this search space", @@ -2724,8 +2733,9 @@ async def create_mcp_connector( async def list_mcp_connectors( search_space_id: int = Query(..., description="Search space ID"), session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ List all MCP connectors for a search space. @@ -2741,7 +2751,7 @@ async def list_mcp_connectors( # Check user has permission to read connectors await check_permission( session, - user, + auth, search_space_id, Permission.CONNECTORS_READ.value, "You don't have permission to view connectors in this search space", @@ -2775,8 +2785,9 @@ async def list_mcp_connectors( async def get_mcp_connector( connector_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Get a specific MCP connector by ID. @@ -2805,7 +2816,7 @@ async def get_mcp_connector( # Check user has permission to read connectors await check_permission( session, - user, + auth, connector.search_space_id, Permission.CONNECTORS_READ.value, "You don't have permission to view this connector", @@ -2828,8 +2839,9 @@ async def update_mcp_connector( connector_id: int, connector_update: MCPConnectorUpdate, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Update an MCP connector. @@ -2859,7 +2871,7 @@ async def update_mcp_connector( # Check user has permission to update connectors await check_permission( session, - user, + auth, connector.search_space_id, Permission.CONNECTORS_UPDATE.value, "You don't have permission to update this connector", @@ -2904,8 +2916,9 @@ async def update_mcp_connector( async def delete_mcp_connector( connector_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Delete an MCP connector. @@ -2931,7 +2944,7 @@ async def delete_mcp_connector( # Check user has permission to delete connectors await check_permission( session, - user, + auth, connector.search_space_id, Permission.CONNECTORS_DELETE.value, "You don't have permission to delete this connector", @@ -2962,8 +2975,9 @@ async def delete_mcp_connector( @router.post("/connectors/mcp/test") async def test_mcp_server_connection( server_config: dict = Body(...), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """ Test connection to an MCP server and fetch available tools. @@ -3042,8 +3056,9 @@ DRIVE_CONNECTOR_TYPES = { async def get_drive_picker_token( connector_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Return an OAuth access token + client ID for the Google Picker API.""" result = await session.execute( select(SearchSourceConnector).filter(SearchSourceConnector.id == connector_id) @@ -3054,7 +3069,7 @@ async def get_drive_picker_token( await check_permission( session, - user, + auth, connector.search_space_id, Permission.CONNECTORS_READ.value, "You don't have permission to access this connector", @@ -3164,8 +3179,9 @@ async def trust_mcp_tool( connector_id: int, body: MCPTrustToolRequest, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Add a tool to the MCP connector's trusted (always-allow) list. Once trusted, the tool executes without HITL approval on subsequent @@ -3209,8 +3225,9 @@ async def untrust_mcp_tool( connector_id: int, body: MCPTrustToolRequest, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): + user = auth.user """Remove a tool from the MCP connector's trusted list. The tool will require HITL approval again on subsequent calls. diff --git a/surfsense_backend/app/routes/team_memory_routes.py b/surfsense_backend/app/routes/team_memory_routes.py index b37a99b03..3ded87d36 100644 --- a/surfsense_backend/app/routes/team_memory_routes.py +++ b/surfsense_backend/app/routes/team_memory_routes.py @@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession +from app.auth.context import AuthContext from app.db import User, get_async_session from app.services.memory import ( MemoryRead, @@ -15,7 +16,7 @@ from app.services.memory import ( reset_memory, save_memory, ) -from app.users import current_active_user +from app.users import get_auth_context from app.utils.rbac import check_search_space_access router = APIRouter() @@ -29,9 +30,10 @@ class TeamMemoryUpdate(BaseModel): async def get_team_memory( search_space_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): - await check_search_space_access(session, user, search_space_id) + user = auth.user + await check_search_space_access(session, auth, search_space_id) memory_md = await read_memory( scope=MemoryScope.TEAM, target_id=search_space_id, @@ -45,9 +47,10 @@ async def update_team_memory( search_space_id: int, body: TeamMemoryUpdate, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): - await check_search_space_access(session, user, search_space_id) + user = auth.user + await check_search_space_access(session, auth, search_space_id) result = await save_memory( scope=MemoryScope.TEAM, target_id=search_space_id, @@ -63,9 +66,10 @@ async def update_team_memory( async def reset_team_memory( search_space_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + auth: AuthContext = Depends(get_auth_context), ): - await check_search_space_access(session, user, search_space_id) + user = auth.user + await check_search_space_access(session, auth, search_space_id) result = await reset_memory( scope=MemoryScope.TEAM, target_id=search_space_id,