From 503c4f5f04d3ceab538c5409094993fa453ad178 Mon Sep 17 00:00:00 2001 From: Aviraj Gour <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:41:24 +0530 Subject: [PATCH 1/9] Refactor dependencies and enhance AuthContext for non-optional organization and project attributes --- backend/app/api/routes/assistants.py | 76 +++++++++++-------- backend/app/api/routes/credentials.py | 51 +++++++------ backend/app/api/routes/openai_conversation.py | 52 +++++++------ backend/app/api/routes/responses.py | 23 +++--- backend/app/api/routes/threads.py | 66 +++++++++------- backend/app/models/auth.py | 15 ++++ 6 files changed, 169 insertions(+), 114 deletions(-) diff --git a/backend/app/api/routes/assistants.py b/backend/app/api/routes/assistants.py index 3ed327f2d..24f7a502f 100644 --- a/backend/app/api/routes/assistants.py +++ b/backend/app/api/routes/assistants.py @@ -1,9 +1,8 @@ from typing import Annotated from fastapi import APIRouter, Depends, Path, HTTPException, Query -from sqlmodel import Session -from app.api.deps import get_db, get_current_user_org_project +from app.api.deps import AuthContextDep, SessionDep from app.crud import ( fetch_assistant_from_openai, sync_assistant, @@ -13,7 +12,8 @@ get_assistants_by_project, delete_assistant, ) -from app.models import UserProjectOrg, AssistantCreate, AssistantUpdate, Assistant +from app.models import AssistantCreate, AssistantUpdate, Assistant +from app.api.permissions import Permission, require_permission from app.utils import APIResponse, get_openai_client router = APIRouter(prefix="/assistant", tags=["Assistants"]) @@ -23,71 +23,81 @@ "/{assistant_id}/ingest", response_model=APIResponse[Assistant], status_code=201, + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def ingest_assistant_route( assistant_id: Annotated[str, Path(description="The ID of the assistant to ingest")], - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + session: SessionDep, + current_user: AuthContextDep, ): """ Ingest an assistant from OpenAI and store it in the platform. """ client = get_openai_client( - session, current_user.organization_id, current_user.project_id + session, current_user.organization_.id, current_user.project_.id ) openai_assistant = fetch_assistant_from_openai(assistant_id, client) assistant = sync_assistant( session=session, - organization_id=current_user.organization_id, - project_id=current_user.project_id, + organization_id=current_user.organization_.id, + project_id=current_user.project_.id, openai_assistant=openai_assistant, ) return APIResponse.success_response(assistant) -@router.post("/", response_model=APIResponse[Assistant], status_code=201) +@router.post( + "/", + response_model=APIResponse[Assistant], + status_code=201, + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], +) def create_assistant_route( assistant_in: AssistantCreate, - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + session: SessionDep, + current_user: AuthContextDep, ): """ Create a new assistant in the local DB, checking that vector store IDs exist in OpenAI first. """ client = get_openai_client( - session, current_user.organization_id, current_user.project_id + session, current_user.organization_.id, current_user.project_.id ) assistant = create_assistant( session=session, openai_client=client, assistant=assistant_in, - project_id=current_user.project_id, - organization_id=current_user.organization_id, + project_id=current_user.project_.id, + organization_id=current_user.organization_.id, ) return APIResponse.success_response(assistant) -@router.patch("/{assistant_id}", response_model=APIResponse[Assistant]) +@router.patch( + "/{assistant_id}", + response_model=APIResponse[Assistant], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], +) def update_assistant_route( assistant_id: Annotated[str, Path(description="Assistant ID to update")], assistant_update: AssistantUpdate, - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + session: SessionDep, + current_user: AuthContextDep, ): """ Update an existing assistant with provided fields. Supports replacing, adding, or removing vector store IDs. """ client = get_openai_client( - session, current_user.organization_id, current_user.project_id + session, current_user.organization_.id, current_user.project_.id ) updated_assistant = update_assistant( session=session, assistant_id=assistant_id, openai_client=client, - project_id=current_user.project_id, + project_id=current_user.project_.id, assistant_update=assistant_update, ) return APIResponse.success_response(updated_assistant) @@ -97,16 +107,17 @@ def update_assistant_route( "/{assistant_id}", response_model=APIResponse[Assistant], summary="Get a single assistant by its ID", + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def get_assistant_route( - assistant_id: str = Path(..., description="The assistant_id to fetch"), - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + assistant_id: Annotated[str, Path(description="The assistant_id to fetch")], + session: SessionDep, + current_user: AuthContextDep, ): """ Fetch a single assistant by its assistant_id. """ - assistant = get_assistant_by_id(session, assistant_id, current_user.project_id) + assistant = get_assistant_by_id(session, assistant_id, current_user.project_.id) if not assistant: raise HTTPException( status_code=404, detail=f"Assistant with ID {assistant_id} not found." @@ -118,10 +129,11 @@ def get_assistant_route( "/", response_model=APIResponse[list[Assistant]], summary="List all assistants in the current project", + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def list_assistants_route( - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + session: SessionDep, + current_user: AuthContextDep, skip: int = Query(0, ge=0, description="How many items to skip"), limit: int = Query(100, ge=1, le=100, description="Maximum items to return"), ): @@ -130,16 +142,20 @@ def list_assistants_route( """ assistants = get_assistants_by_project( - session=session, project_id=current_user.project_id, skip=skip, limit=limit + session=session, project_id=current_user.project_.id, skip=skip, limit=limit ) return APIResponse.success_response(assistants) -@router.delete("/{assistant_id}", response_model=APIResponse) +@router.delete( + "/{assistant_id}", + response_model=APIResponse, + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], +) def delete_assistant_route( assistant_id: Annotated[str, Path(description="Assistant ID to delete")], - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + session: SessionDep, + current_user: AuthContextDep, ): """ Soft delete an assistant by marking it as deleted. @@ -147,7 +163,7 @@ def delete_assistant_route( delete_assistant( session=session, assistant_id=assistant_id, - project_id=current_user.project_id, + project_id=current_user.project_.id, ) return APIResponse.success_response( data={"message": "Assistant deleted successfully."} diff --git a/backend/app/api/routes/credentials.py b/backend/app/api/routes/credentials.py index c502b1e2d..74e7d9622 100644 --- a/backend/app/api/routes/credentials.py +++ b/backend/app/api/routes/credentials.py @@ -2,7 +2,8 @@ from fastapi import APIRouter, Depends -from app.api.deps import SessionDep, get_current_user_org_project +from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.core.exception_handlers import HTTPException from app.core.providers import validate_provider from app.crud.credentials import ( @@ -13,7 +14,7 @@ set_creds_for_org, update_creds_for_org, ) -from app.models import CredsCreate, CredsPublic, CredsUpdate, UserProjectOrg +from app.models import CredsCreate, CredsPublic, CredsUpdate from app.utils import APIResponse, load_description logger = logging.getLogger(__name__) @@ -24,12 +25,13 @@ "/", response_model=APIResponse[list[CredsPublic]], description=load_description("credentials/create.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def create_new_credential( *, session: SessionDep, creds_in: CredsCreate, - _current_user: UserProjectOrg = Depends(get_current_user_org_project), + _current_user: AuthContextDep, ): # Project comes from API key context; no cross-org check needed here # Database unique constraint ensures no duplicate credentials per provider-org-project combination @@ -37,12 +39,12 @@ def create_new_credential( created_creds = set_creds_for_org( session=session, creds_add=creds_in, - organization_id=_current_user.organization_id, - project_id=_current_user.project_id, + organization_id=_current_user.organization_.id, + project_id=_current_user.project_.id, ) if not created_creds: logger.error( - f"[create_new_credential] Failed to create credentials | organization_id: {_current_user.organization_id}, project_id: {_current_user.project_id}" + f"[create_new_credential] Failed to create credentials | organization_id: {_current_user.organization_.id}, project_id: {_current_user.project_.id}" ) raise HTTPException(status_code=500, detail="Failed to create credentials") @@ -53,16 +55,17 @@ def create_new_credential( "/", response_model=APIResponse[list[CredsPublic]], description=load_description("credentials/list.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def read_credential( *, session: SessionDep, - _current_user: UserProjectOrg = Depends(get_current_user_org_project), + _current_user: AuthContextDep, ): creds = get_creds_by_org( session=session, - org_id=_current_user.organization_id, - project_id=_current_user.project_id, + org_id=_current_user.organization_.id, + project_id=_current_user.project_.id, ) if not creds: raise HTTPException(status_code=404, detail="Credentials not found") @@ -74,19 +77,20 @@ def read_credential( "/provider/{provider}", response_model=APIResponse[dict], description=load_description("credentials/get_provider.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def read_provider_credential( *, session: SessionDep, provider: str, - _current_user: UserProjectOrg = Depends(get_current_user_org_project), + _current_user: AuthContextDep, ): provider_enum = validate_provider(provider) credential = get_provider_credential( session=session, - org_id=_current_user.organization_id, + org_id=_current_user.organization_.id, provider=provider_enum, - project_id=_current_user.project_id, + project_id=_current_user.project_.id, ) if credential is None: raise HTTPException(status_code=404, detail="Provider credentials not found") @@ -98,16 +102,17 @@ def read_provider_credential( "/", response_model=APIResponse[list[CredsPublic]], description=load_description("credentials/update.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def update_credential( *, session: SessionDep, creds_in: CredsUpdate, - _current_user: UserProjectOrg = Depends(get_current_user_org_project), + _current_user: AuthContextDep, ): if not creds_in or not creds_in.provider or not creds_in.credential: logger.error( - f"[update_credential] Invalid input | organization_id: {_current_user.organization_id}, project_id: {_current_user.project_id}" + f"[update_credential] Invalid input | organization_id: {_current_user.organization_.id}, project_id: {_current_user.project_.id}" ) raise HTTPException( status_code=400, detail="Provider and credential must be provided" @@ -116,9 +121,9 @@ def update_credential( # Pass project_id directly to the CRUD function since CredsUpdate no longer has this field updated_credential = update_creds_for_org( session=session, - org_id=_current_user.organization_id, + org_id=_current_user.organization_.id, creds_in=creds_in, - project_id=_current_user.project_id, + project_id=_current_user.project_.id, ) return APIResponse.success_response( @@ -130,19 +135,20 @@ def update_credential( "/provider/{provider}", response_model=APIResponse[dict], description=load_description("credentials/delete_provider.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def delete_provider_credential( *, session: SessionDep, provider: str, - _current_user: UserProjectOrg = Depends(get_current_user_org_project), + _current_user: AuthContextDep, ): provider_enum = validate_provider(provider) remove_provider_credential( session=session, - org_id=_current_user.organization_id, + org_id=_current_user.organization_.id, provider=provider_enum, - project_id=_current_user.project_id, + project_id=_current_user.project_.id, ) return APIResponse.success_response( @@ -154,16 +160,17 @@ def delete_provider_credential( "/", response_model=APIResponse[dict], description=load_description("credentials/delete_all.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def delete_all_credentials( *, session: SessionDep, - _current_user: UserProjectOrg = Depends(get_current_user_org_project), + _current_user: AuthContextDep, ): remove_creds_for_org( session=session, - org_id=_current_user.organization_id, - project_id=_current_user.project_id, + org_id=_current_user.organization_.id, + project_id=_current_user.project_.id, ) return APIResponse.success_response( diff --git a/backend/app/api/routes/openai_conversation.py b/backend/app/api/routes/openai_conversation.py index b206bf5bd..de0be838f 100644 --- a/backend/app/api/routes/openai_conversation.py +++ b/backend/app/api/routes/openai_conversation.py @@ -1,9 +1,9 @@ from typing import Annotated from fastapi import APIRouter, Depends, Path, HTTPException, Query -from sqlmodel import Session -from app.api.deps import get_db, get_current_user_org_project +from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.crud import ( get_conversation_by_id, get_conversation_by_response_id, @@ -13,7 +13,6 @@ delete_conversation, ) from app.models import ( - UserProjectOrg, OpenAIConversationPublic, ) from app.utils import APIResponse, load_description @@ -26,17 +25,18 @@ response_model=APIResponse[OpenAIConversationPublic], summary="Get a single conversation by its ID", description=load_description("openai_conversation/get.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def get_conversation_route( - conversation_id: int = Path(..., description="The conversation ID to fetch"), - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + conversation_id: Annotated[int, Path(description="The conversation ID to fetch")], + session: SessionDep, + current_user: AuthContextDep, ): """ Fetch a single conversation by its ID. """ conversation = get_conversation_by_id( - session, conversation_id, current_user.project_id + session, conversation_id, current_user.project_.id ) if not conversation: raise HTTPException( @@ -50,17 +50,18 @@ def get_conversation_route( response_model=APIResponse[OpenAIConversationPublic], summary="Get a conversation by its OpenAI response ID", description=load_description("openai_conversation/get_by_response_id.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def get_conversation_by_response_id_route( - response_id: str = Path(..., description="The OpenAI response ID to fetch"), - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + response_id: Annotated[str, Path(description="The OpenAI response ID to fetch")], + session: SessionDep, + current_user: AuthContextDep, ): """ Fetch a conversation by its OpenAI response ID. """ conversation = get_conversation_by_response_id( - session, response_id, current_user.project_id + session, response_id, current_user.project_.id ) if not conversation: raise HTTPException( @@ -75,19 +76,20 @@ def get_conversation_by_response_id_route( response_model=APIResponse[OpenAIConversationPublic], summary="Get a conversation by its ancestor response ID", description=load_description("openai_conversation/get_by_ancestor_id.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def get_conversation_by_ancestor_id_route( - ancestor_response_id: str = Path( - ..., description="The ancestor response ID to fetch" - ), - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + ancestor_response_id: Annotated[ + str, Path(description="The ancestor response ID to fetch") + ], + session: SessionDep, + current_user: AuthContextDep, ): """ Fetch a conversation by its ancestor response ID. """ conversation = get_conversation_by_ancestor_id( - session, ancestor_response_id, current_user.project_id + session, ancestor_response_id, current_user.project_.id ) if not conversation: raise HTTPException( @@ -102,10 +104,11 @@ def get_conversation_by_ancestor_id_route( response_model=APIResponse[list[OpenAIConversationPublic]], summary="List all conversations in the current project", description=load_description("openai_conversation/list.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def list_conversations_route( - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + session: SessionDep, + current_user: AuthContextDep, skip: int = Query(0, ge=0, description="How many items to skip"), limit: int = Query(100, ge=1, le=100, description="Maximum items to return"), ): @@ -114,7 +117,7 @@ def list_conversations_route( """ conversations = get_conversations_by_project( session=session, - project_id=current_user.project_id, + project_id=current_user.project_.id, skip=skip, # ← Pagination offset limit=limit, # ← Page size ) @@ -122,7 +125,7 @@ def list_conversations_route( # Get total count for pagination metadata total = get_conversations_count_by_project( session=session, - project_id=current_user.project_id, + project_id=current_user.project_.id, ) return APIResponse.success_response( @@ -134,11 +137,12 @@ def list_conversations_route( "/{conversation_id}", response_model=APIResponse, description=load_description("openai_conversation/delete.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def delete_conversation_route( conversation_id: Annotated[int, Path(description="Conversation ID to delete")], - session: Session = Depends(get_db), - current_user: UserProjectOrg = Depends(get_current_user_org_project), + session: SessionDep, + current_user: AuthContextDep, ): """ Soft delete a conversation by marking it as deleted. @@ -146,7 +150,7 @@ def delete_conversation_route( deleted_conversation = delete_conversation( session=session, conversation_id=conversation_id, - project_id=current_user.project_id, + project_id=current_user.project_.id, ) if not deleted_conversation: diff --git a/backend/app/api/routes/responses.py b/backend/app/api/routes/responses.py index c5ebcaa64..6426270bc 100644 --- a/backend/app/api/routes/responses.py +++ b/backend/app/api/routes/responses.py @@ -3,9 +3,9 @@ import openai from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import JSONResponse -from sqlmodel import Session -from app.api.deps import get_db, get_current_user_org_project +from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.core.langfuse.langfuse import LangfuseTracer from app.crud.credentials import get_provider_credential from app.models import ( @@ -14,7 +14,6 @@ ResponsesAPIRequest, ResponseJobStatus, ResponsesSyncAPIRequest, - UserProjectOrg, ) from app.services.response.jobs import start_job from app.services.response.response import get_file_search_results @@ -35,16 +34,17 @@ "/responses", response_model=APIResponse[ResponseJobStatus], description=load_description("responses/create_async.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) async def responses( request: ResponsesAPIRequest, - _session: Session = Depends(get_db), - _current_user: UserProjectOrg = Depends(get_current_user_org_project), + _session: SessionDep, + _current_user: AuthContextDep, ): """Asynchronous endpoint that processes requests using Celery.""" project_id, organization_id = ( - _current_user.project_id, - _current_user.organization_id, + _current_user.project_.id, + _current_user.organization_.id, ) start_job( @@ -69,16 +69,17 @@ async def responses( "/responses/sync", response_model=APIResponse[CallbackResponse], description=load_description("responses/create_sync.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) async def responses_sync( request: ResponsesSyncAPIRequest, - _session: Session = Depends(get_db), - _current_user: UserProjectOrg = Depends(get_current_user_org_project), + _session: SessionDep, + _current_user: AuthContextDep, ): """Synchronous endpoint for benchmarking OpenAI responses API with Langfuse tracing.""" project_id, organization_id = ( - _current_user.project_id, - _current_user.organization_id, + _current_user.project_.id, + _current_user.organization_.id, ) try: diff --git a/backend/app/api/routes/threads.py b/backend/app/api/routes/threads.py index b42830d7d..92df1735f 100644 --- a/backend/app/api/routes/threads.py +++ b/backend/app/api/routes/threads.py @@ -5,12 +5,12 @@ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from openai import OpenAI from pydantic import BaseModel, Field -from sqlmodel import Session from typing import Optional -from app.api.deps import get_current_user_org, get_db, get_current_user_org_project +from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.core import logging, settings -from app.models import UserOrganization, OpenAIThreadCreate, UserProjectOrg +from app.models import OpenAIThreadCreate from app.crud import upsert_thread_result, get_thread_result from app.utils import APIResponse, mask_string from app.crud.credentials import get_provider_credential @@ -286,24 +286,27 @@ def poll_run_and_prepare_response(request: dict, client: OpenAI, db: Session): ) -@router.post("/threads") +@router.post( + "/threads", + dependencies=[Depends(require_permission(Permission.REQUIRE_ORGANIZATION))], +) async def threads( request: dict, background_tasks: BackgroundTasks, - _session: Session = Depends(get_db), - _current_user: UserOrganization = Depends(get_current_user_org), + _session: SessionDep, + _current_user: AuthContextDep, ): """Asynchronous endpoint that processes requests in background.""" credentials = get_provider_credential( session=_session, - org_id=_current_user.organization_id, + org_id=_current_user.organization_.id, provider="openai", project_id=request.get("project_id"), ) client, success = configure_openai(credentials) if not success: logger.error( - f"[threads] OpenAI API key not configured for this organization. | organization_id: {_current_user.organization_id}, project_id: {request.get('project_id')}" + f"[threads] OpenAI API key not configured for this organization. | organization_id: {_current_user.organization_.id}, project_id: {request.get('project_id')}" ) return APIResponse.failure_response( error="OpenAI API key not configured for this organization." @@ -311,7 +314,7 @@ async def threads( langfuse_credentials = get_provider_credential( session=_session, - org_id=_current_user.organization_id, + org_id=_current_user.organization_.id, provider="langfuse", project_id=request.get("project_id"), ) @@ -351,21 +354,24 @@ async def threads( # Schedule background task background_tasks.add_task(process_run, request, client, tracer) logger.info( - f"[threads] Background task scheduled for thread ID: {mask_string(request.get('thread_id'))} | organization_id: {_current_user.organization_id}, project_id: {request.get('project_id')}" + f"[threads] Background task scheduled for thread ID: {mask_string(request.get('thread_id'))} | organization_id: {_current_user.organization_.id}, project_id: {request.get('project_id')}" ) return initial_response -@router.post("/threads/sync") +@router.post( + "/threads/sync", + dependencies=[Depends(require_permission(Permission.REQUIRE_ORGANIZATION))], +) async def threads_sync( request: dict, - _session: Session = Depends(get_db), - _current_user: UserOrganization = Depends(get_current_user_org), + _session: SessionDep, + _current_user: AuthContextDep, ): """Synchronous endpoint that processes requests immediately.""" credentials = get_provider_credential( session=_session, - org_id=_current_user.organization_id, + org_id=_current_user.organization_.id, provider="openai", project_id=request.get("project_id"), ) @@ -374,7 +380,7 @@ async def threads_sync( client, success = configure_openai(credentials) if not success: logger.error( - f"[threads_sync] OpenAI API key not configured for this organization. | organization_id: {_current_user.organization_id}, project_id: {request.get('project_id')}" + f"[threads_sync] OpenAI API key not configured for this organization. | organization_id: {_current_user.organization_.id}, project_id: {request.get('project_id')}" ) return APIResponse.failure_response( error="OpenAI API key not configured for this organization." @@ -383,7 +389,7 @@ async def threads_sync( # Get Langfuse credentials langfuse_credentials = get_provider_credential( session=_session, - org_id=_current_user.organization_id, + org_id=_current_user.organization_.id, provider="langfuse", project_id=request.get("project_id"), ) @@ -416,12 +422,15 @@ async def threads_sync( return response -@router.post("/threads/start") +@router.post( + "/threads/start", + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], +) async def start_thread( request: StartThreadRequest, background_tasks: BackgroundTasks, - db: Session = Depends(get_db), - _current_user: UserProjectOrg = Depends(get_current_user_org_project), + db: SessionDep, + _current_user: AuthContextDep, ): """ Create a new OpenAI thread for the given question and start polling in the background. @@ -430,16 +439,16 @@ async def start_thread( prompt = request["question"] credentials = get_provider_credential( session=db, - org_id=_current_user.organization_id, + org_id=_current_user.organization_.id, provider="openai", - project_id=_current_user.project_id, + project_id=_current_user.project_.id, ) # Configure OpenAI client client, success = configure_openai(credentials) if not success: logger.error( - f"[start_thread] OpenAI API key not configured for this organization. | project_id: {_current_user.project_id}" + f"[start_thread] OpenAI API key not configured for this organization. | project_id: {_current_user.project_.id}" ) return APIResponse.failure_response( error="OpenAI API key not configured for this organization." @@ -465,7 +474,7 @@ async def start_thread( background_tasks.add_task(poll_run_and_prepare_response, request, client, db) logger.info( - f"[start_thread] Background task scheduled to process response for thread ID: {mask_string(thread_id)} | project_id: {_current_user.project_id}" + f"[start_thread] Background task scheduled to process response for thread ID: {mask_string(thread_id)} | project_id: {_current_user.project_.id}" ) return APIResponse.success_response( data={ @@ -477,11 +486,14 @@ async def start_thread( ) -@router.get("/threads/result/{thread_id}") +@router.get( + "/threads/result/{thread_id}", + dependencies=[Depends(require_permission(Permission.REQUIRE_ORGANIZATION))], +) async def get_thread( thread_id: str, - db: Session = Depends(get_db), - _current_user: UserOrganization = Depends(get_current_user_org), + db: SessionDep, + _current_user: AuthContextDep, ): """ Retrieve the result of a previously started OpenAI thread using its thread ID. @@ -490,7 +502,7 @@ async def get_thread( if not result: logger.error( - f"[get_thread] Thread result not found for ID: {mask_string(thread_id)} | org_id: {_current_user.organization_id}" + f"[get_thread] Thread result not found for ID: {mask_string(thread_id)} | org_id: {_current_user.organization_.id}" ) raise HTTPException(404, "thread not found") diff --git a/backend/app/models/auth.py b/backend/app/models/auth.py index adb93aeb9..26b42ef8a 100644 --- a/backend/app/models/auth.py +++ b/backend/app/models/auth.py @@ -2,6 +2,7 @@ from app.models.user import User from app.models.organization import Organization from app.models.project import Project +from typing import TYPE_CHECKING # JSON payload containing access token @@ -19,3 +20,17 @@ class AuthContext(SQLModel): user: User organization: Organization | None = None project: Project | None = None + + @property + def organization_(self) -> Organization: + """Non-optional organization - raises if None""" + if self.organization is None: + raise ValueError("Organization is required but was None") + return self.organization + + @property + def project_(self) -> Project: + """Non-optional project - raises if None""" + if self.project is None: + raise ValueError("Project is required but was None") + return self.project From 9cce4d8d49f1cdcfb89c049622a922c7857be697 Mon Sep 17 00:00:00 2001 From: Aviraj Gour <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:46:15 +0530 Subject: [PATCH 2/9] Refactor permission checks to require SUPERUSER role across multiple routes --- backend/app/api/routes/llm.py | 4 ++-- backend/app/api/routes/onboarding.py | 8 +++----- backend/app/api/routes/organization.py | 16 +++++++--------- backend/app/api/routes/project.py | 16 +++++++--------- backend/app/api/routes/utils.py | 4 ++-- 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/backend/app/api/routes/llm.py b/backend/app/api/routes/llm.py index e244b2258..239f1a5ba 100644 --- a/backend/app/api/routes/llm.py +++ b/backend/app/api/routes/llm.py @@ -42,8 +42,8 @@ def llm_call( """ Endpoint to initiate an LLM call as a background job. """ - project_id = _current_user.project.id - organization_id = _current_user.organization.id + project_id = _current_user.project_.id + organization_id = _current_user.organization_.id if request.callback_url: validate_callback_url(str(request.callback_url)) diff --git a/backend/app/api/routes/onboarding.py b/backend/app/api/routes/onboarding.py index 9502a91d4..c5f7f1231 100644 --- a/backend/app/api/routes/onboarding.py +++ b/backend/app/api/routes/onboarding.py @@ -1,9 +1,7 @@ from fastapi import APIRouter, Depends -from app.api.deps import ( - SessionDep, - get_current_active_superuser, -) +from app.api.deps import SessionDep +from app.api.permissions import Permission, require_permission from app.crud import onboard_project from app.models import OnboardingRequest, OnboardingResponse, User from app.utils import APIResponse, load_description @@ -16,11 +14,11 @@ response_model=APIResponse[OnboardingResponse], status_code=201, description=load_description("onboarding/onboarding.md"), + dependencies=[Depends(require_permission(Permission.SUPERUSER))], ) def onboard_project_route( onboard_in: OnboardingRequest, session: SessionDep, - current_user: User = Depends(get_current_active_superuser), ): response = onboard_project(session=session, onboard_in=onboard_in) diff --git a/backend/app/api/routes/organization.py b/backend/app/api/routes/organization.py index a25873a6d..eb853921b 100644 --- a/backend/app/api/routes/organization.py +++ b/backend/app/api/routes/organization.py @@ -11,10 +11,8 @@ OrganizationUpdate, OrganizationPublic, ) -from app.api.deps import ( - SessionDep, - get_current_active_superuser, -) +from app.api.deps import SessionDep +from app.api.permissions import Permission, require_permission from app.crud.organization import create_organization, get_organization_by_id from app.utils import APIResponse, load_description @@ -25,7 +23,7 @@ # Retrieve organizations @router.get( "/", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=APIResponse[List[OrganizationPublic]], description=load_description("organization/list.md"), ) @@ -42,7 +40,7 @@ def read_organizations(session: SessionDep, skip: int = 0, limit: int = 100): # Create a new organization @router.post( "/", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=APIResponse[OrganizationPublic], description=load_description("organization/create.md"), ) @@ -53,7 +51,7 @@ def create_new_organization(*, session: SessionDep, org_in: OrganizationCreate): @router.get( "/{org_id}", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=APIResponse[OrganizationPublic], description=load_description("organization/get.md"), ) @@ -71,7 +69,7 @@ def read_organization(*, session: SessionDep, org_id: int): # Update an organization @router.patch( "/{org_id}", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=APIResponse[OrganizationPublic], description=load_description("organization/update.md"), ) @@ -100,7 +98,7 @@ def update_organization( # Delete an organization @router.delete( "/{org_id}", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=APIResponse[None], include_in_schema=False, description=load_description("organization/delete.md"), diff --git a/backend/app/api/routes/project.py b/backend/app/api/routes/project.py index 79c6a1314..8a114930d 100644 --- a/backend/app/api/routes/project.py +++ b/backend/app/api/routes/project.py @@ -6,10 +6,8 @@ from sqlmodel import select from app.models import Project, ProjectCreate, ProjectUpdate, ProjectPublic -from app.api.deps import ( - SessionDep, - get_current_active_superuser, -) +from app.api.deps import SessionDep +from app.api.permissions import Permission, require_permission from app.crud.project import ( create_project, get_project_by_id, @@ -23,7 +21,7 @@ # Retrieve projects @router.get( "/", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=APIResponse[List[ProjectPublic]], description=load_description("projects/list.md"), ) @@ -44,7 +42,7 @@ def read_projects( # Create a new project @router.post( "/", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=APIResponse[ProjectPublic], description=load_description("projects/create.md"), ) @@ -55,7 +53,7 @@ def create_new_project(*, session: SessionDep, project_in: ProjectCreate): @router.get( "/{project_id}", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=APIResponse[ProjectPublic], description=load_description("projects/get.md"), ) @@ -73,7 +71,7 @@ def read_project(*, session: SessionDep, project_id: int): # Update a project @router.patch( "/{project_id}", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=APIResponse[ProjectPublic], description=load_description("projects/update.md"), ) @@ -98,7 +96,7 @@ def update_project(*, session: SessionDep, project_id: int, project_in: ProjectU # Delete a project @router.delete( "/{project_id}", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], include_in_schema=False, description=load_description("projects/delete.md"), ) diff --git a/backend/app/api/routes/utils.py b/backend/app/api/routes/utils.py index 294f729c7..56247b304 100644 --- a/backend/app/api/routes/utils.py +++ b/backend/app/api/routes/utils.py @@ -1,16 +1,16 @@ from fastapi import APIRouter, Depends from pydantic.networks import EmailStr -from app.api.deps import get_current_active_superuser from app.models import Message from app.utils import generate_test_email, send_email +from app.api.permissions import Permission, require_permission router = APIRouter(prefix="/utils", tags=["utils"]) @router.post( "/test-email/", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], status_code=201, include_in_schema=False, ) From f54a96b6a6bc2c134285a414ba55df581003b3af Mon Sep 17 00:00:00 2001 From: Aviraj Gour <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:51:12 +0530 Subject: [PATCH 3/9] fix session --- backend/app/api/routes/threads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/api/routes/threads.py b/backend/app/api/routes/threads.py index 92df1735f..2ea34be61 100644 --- a/backend/app/api/routes/threads.py +++ b/backend/app/api/routes/threads.py @@ -242,7 +242,7 @@ def process_run(request: dict, client: OpenAI, tracer: LangfuseTracer): send_callback(request["callback_url"], response) -def poll_run_and_prepare_response(request: dict, client: OpenAI, db: Session): +def poll_run_and_prepare_response(request: dict, client: OpenAI, db: SessionDep): """Handles a thread run, processes the response, and upserts the result to the database.""" thread_id = request["thread_id"] prompt = request["question"] From 567d6e2b04698e7fec41f11dd17044d47e6fc780 Mon Sep 17 00:00:00 2001 From: Aviraj Gour <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:13:21 +0530 Subject: [PATCH 4/9] Refactor routes to enhance AuthContext usage and enforce project permissions --- backend/app/api/routes/collection_job.py | 12 +-- backend/app/api/routes/collections.py | 39 ++++---- .../app/api/routes/doc_transformation_job.py | 23 ++--- backend/app/api/routes/documents.py | 47 +++++----- backend/app/api/routes/evaluation.py | 88 ++++++++++--------- backend/app/api/routes/fine_tuning.py | 50 ++++++----- backend/app/api/routes/model_evaluation.py | 60 +++++++------ 7 files changed, 176 insertions(+), 143 deletions(-) diff --git a/backend/app/api/routes/collection_job.py b/backend/app/api/routes/collection_job.py index c16fa6586..31686c83e 100644 --- a/backend/app/api/routes/collection_job.py +++ b/backend/app/api/routes/collection_job.py @@ -1,11 +1,12 @@ import logging from uuid import UUID -from fastapi import APIRouter +from fastapi import APIRouter, Depends from fastapi import Path as FastPath -from app.api.deps import SessionDep, CurrentUserOrgProject +from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.crud import ( CollectionCrud, CollectionJobCrud, @@ -29,13 +30,14 @@ "/jobs/{job_id}", description=load_description("collections/job_info.md"), response_model=APIResponse[CollectionJobPublic], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def collection_job_info( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, job_id: UUID = FastPath(description="Collection job to retrieve"), ): - collection_job_crud = CollectionJobCrud(session, current_user.project_id) + collection_job_crud = CollectionJobCrud(session, current_user.project_.id) collection_job = collection_job_crud.read_one(job_id) job_out = CollectionJobPublic.model_validate(collection_job) @@ -45,7 +47,7 @@ def collection_job_info( collection_job.action_type == CollectionActionType.CREATE and collection_job.status == CollectionJobStatus.SUCCESSFUL ): - collection_crud = CollectionCrud(session, current_user.project_id) + collection_crud = CollectionCrud(session, current_user.project_.id) collection = collection_crud.read_one(collection_job.collection_id) job_out.collection = CollectionPublic.model_validate(collection) diff --git a/backend/app/api/routes/collections.py b/backend/app/api/routes/collections.py index a78c05fde..d19fad31f 100644 --- a/backend/app/api/routes/collections.py +++ b/backend/app/api/routes/collections.py @@ -2,10 +2,11 @@ from uuid import UUID from typing import List -from fastapi import APIRouter, Query, Body +from fastapi import APIRouter, Query, Body, Depends from fastapi import Path as FastPath -from app.api.deps import SessionDep, CurrentUserOrgProject +from app.api.deps import SessionDep, AuthContextDep +from app.api.permissions import Permission, require_permission from app.crud import ( CollectionCrud, CollectionJobCrud, @@ -59,12 +60,13 @@ def collection_callback_notification(body: APIResponse[CollectionJobPublic]): "/", description=load_description("collections/list.md"), response_model=APIResponse[List[CollectionPublic]], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def list_collections( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, ): - collection_crud = CollectionCrud(session, current_user.project_id) + collection_crud = CollectionCrud(session, current_user.project_.id) rows = collection_crud.read_all() return APIResponse.success_response(rows) @@ -75,20 +77,21 @@ def list_collections( description=load_description("collections/create.md"), response_model=APIResponse[CollectionJobImmediatePublic], callbacks=collection_callback_router.routes, + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def create_collection( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, request: CreationRequest, ): if request.callback_url: validate_callback_url(str(request.callback_url)) - collection_job_crud = CollectionJobCrud(session, current_user.project_id) + collection_job_crud = CollectionJobCrud(session, current_user.project_.id) collection_job = collection_job_crud.create( CollectionJobCreate( action_type=CollectionActionType.CREATE, - project_id=current_user.project_id, + project_id=current_user.project_.id, status=CollectionJobStatus.PENDING, ) ) @@ -102,8 +105,8 @@ def create_collection( db=session, request=request, collection_job_id=collection_job.id, - project_id=current_user.project_id, - organization_id=current_user.organization_id, + project_id=current_user.project_.id, + organization_id=current_user.organization_.id, with_assistant=with_assistant, ) @@ -126,28 +129,29 @@ def create_collection( description=load_description("collections/delete.md"), response_model=APIResponse[CollectionJobImmediatePublic], callbacks=collection_callback_router.routes, + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def delete_collection( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, collection_id: UUID = FastPath(description="Collection to delete"), request: CallbackRequest | None = Body(default=None), ): if request and request.callback_url: validate_callback_url(str(request.callback_url)) - _ = CollectionCrud(session, current_user.project_id).read_one(collection_id) + _ = CollectionCrud(session, current_user.project_.id).read_one(collection_id) deletion_request = DeletionRequest( collection_id=collection_id, callback_url=request.callback_url if request else None, ) - collection_job_crud = CollectionJobCrud(session, current_user.project_id) + collection_job_crud = CollectionJobCrud(session, current_user.project_.id) collection_job = collection_job_crud.create( CollectionJobCreate( action_type=CollectionActionType.DELETE, - project_id=current_user.project_id, + project_id=current_user.project_.id, status=CollectionJobStatus.PENDING, collection_id=collection_id, ) @@ -157,8 +161,8 @@ def delete_collection( db=session, request=deletion_request, collection_job_id=collection_job.id, - project_id=current_user.project_id, - organization_id=current_user.organization_id, + project_id=current_user.project_.id, + organization_id=current_user.organization_.id, ) return APIResponse.success_response( @@ -170,10 +174,11 @@ def delete_collection( "/{collection_id}", description=load_description("collections/info.md"), response_model=APIResponse[CollectionWithDocsPublic], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def collection_info( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, collection_id: UUID = FastPath(description="Collection to retrieve"), include_docs: bool = Query( True, @@ -182,7 +187,7 @@ def collection_info( skip: int = Query(0, ge=0), limit: int = Query(100, gt=0, le=100), ): - collection_crud = CollectionCrud(session, current_user.project_id) + collection_crud = CollectionCrud(session, current_user.project_.id) collection = collection_crud.read_one(collection_id) collection_with_docs = CollectionWithDocsPublic.model_validate(collection) diff --git a/backend/app/api/routes/doc_transformation_job.py b/backend/app/api/routes/doc_transformation_job.py index e3636446c..7af491af9 100644 --- a/backend/app/api/routes/doc_transformation_job.py +++ b/backend/app/api/routes/doc_transformation_job.py @@ -1,9 +1,10 @@ from uuid import UUID import logging -from fastapi import APIRouter, Query, Path +from fastapi import APIRouter, Depends, Query, Path -from app.api.deps import CurrentUserOrgProject, SessionDep +from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.crud import DocTransformationJobCrud, DocumentCrud from app.models import ( DocTransformationJobPublic, @@ -22,21 +23,22 @@ "/{job_id}", description=load_description("documents/job_info.md"), response_model=APIResponse[DocTransformationJobPublic], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def get_transformation_job( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, job_id: UUID = Path(..., description="Transformation job ID"), include_url: bool = Query( False, description="Include a signed URL for the transformed document" ), ): - job_crud = DocTransformationJobCrud(session, current_user.project_id) - doc_crud = DocumentCrud(session, current_user.project_id) + job_crud = DocTransformationJobCrud(session, current_user.project_.id) + doc_crud = DocumentCrud(session, current_user.project_.id) job = job_crud.read_one(job_id) storage = ( - get_cloud_storage(session=session, project_id=current_user.project_id) + get_cloud_storage(session=session, project_id=current_user.project_.id) if include_url else None ) @@ -54,10 +56,11 @@ def get_transformation_job( "/", description=load_description("documents/job_list.md"), response_model=APIResponse[DocTransformationJobsPublic], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def get_multiple_transformation_jobs( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, job_ids: list[UUID] = Query( ..., description="List of transformation job IDs", @@ -68,15 +71,15 @@ def get_multiple_transformation_jobs( False, description="Include a signed URL for each transformed document" ), ): - job_crud = DocTransformationJobCrud(session, project_id=current_user.project_id) - doc_crud = DocumentCrud(session, project_id=current_user.project_id) + job_crud = DocTransformationJobCrud(session, project_id=current_user.project_.id) + doc_crud = DocumentCrud(session, project_id=current_user.project_.id) jobs = job_crud.read_each(set(job_ids)) jobs_found_ids = {job.id for job in jobs} jobs_not_found = set(job_ids) - jobs_found_ids storage = ( - get_cloud_storage(session=session, project_id=current_user.project_id) + get_cloud_storage(session=session, project_id=current_user.project_.id) if include_url else None ) diff --git a/backend/app/api/routes/documents.py b/backend/app/api/routes/documents.py index f0ca66e57..706f0f269 100644 --- a/backend/app/api/routes/documents.py +++ b/backend/app/api/routes/documents.py @@ -5,6 +5,7 @@ from fastapi import ( APIRouter, + Depends, File, Form, Query, @@ -13,7 +14,8 @@ from pydantic import HttpUrl from fastapi import Path as FastPath -from app.api.deps import CurrentUserOrgProject, SessionDep +from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.core.cloud import get_cloud_storage from app.crud import CollectionCrud, DocumentCrud from app.crud.rag import OpenAIAssistantCrud, OpenAIVectorStoreCrud @@ -68,21 +70,22 @@ def doctransformation_callback_notification( "/", description=load_description("documents/list.md"), response_model=APIResponse[list[Union[DocumentPublic, TransformedDocumentPublic]]], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def list_docs( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, skip: int = Query(0, ge=0), limit: int = Query(100, gt=0, le=100), include_url: bool = Query( False, description="Include a signed URL to access each document" ), ): - crud = DocumentCrud(session, current_user.project_id) + crud = DocumentCrud(session, current_user.project_.id) documents = crud.read_many(skip, limit) storage = ( - get_cloud_storage(session=session, project_id=current_user.project_id) + get_cloud_storage(session=session, project_id=current_user.project_.id) if include_url and documents else None ) @@ -100,10 +103,11 @@ def list_docs( description=load_description("documents/upload.md"), response_model=APIResponse[DocumentUploadResponse], callbacks=doctransformation_callback_router.routes, + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) async def upload_doc( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, src: UploadFile = File(...), target_format: str | None = Form( @@ -126,11 +130,11 @@ async def upload_doc( transformer=transformer, ) - storage = get_cloud_storage(session=session, project_id=current_user.project_id) + storage = get_cloud_storage(session=session, project_id=current_user.project_.id) document_id = uuid4() object_store_url = storage.put(src, Path(str(document_id))) - crud = DocumentCrud(session, current_user.project_id) + crud = DocumentCrud(session, current_user.project_.id) document = Document( id=document_id, fname=src.filename, @@ -140,7 +144,7 @@ async def upload_doc( job_info: TransformationJobInfo | None = schedule_transformation( session=session, - project_id=current_user.project_id, + project_id=current_user.project_.id, current_user=current_user, source_format=source_format, target_format=target_format, @@ -167,20 +171,21 @@ async def upload_doc( "/{doc_id}", description=load_description("documents/delete.md"), response_model=APIResponse[Message], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def remove_doc( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, doc_id: UUID = FastPath(description="Document to delete"), ): client = get_openai_client( - session, current_user.organization_id, current_user.project_id + session, current_user.organization_.id, current_user.project_.id ) a_crud = OpenAIAssistantCrud(client) v_crud = OpenAIVectorStoreCrud(client) - d_crud = DocumentCrud(session, current_user.project_id) - c_crud = CollectionCrud(session, current_user.project_id) + d_crud = DocumentCrud(session, current_user.project_.id) + c_crud = CollectionCrud(session, current_user.project_.id) document = d_crud.read_one(doc_id) remote = pick_service_for_documennt( @@ -198,20 +203,21 @@ def remove_doc( "/{doc_id}/permanent", description=load_description("documents/permanent_delete.md"), response_model=APIResponse[Message], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def permanent_delete_doc( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, doc_id: UUID = FastPath(description="Document to permanently delete"), ): client = get_openai_client( - session, current_user.organization_id, current_user.project_id + session, current_user.organization_.id, current_user.project_.id ) a_crud = OpenAIAssistantCrud(client) v_crud = OpenAIVectorStoreCrud(client) - d_crud = DocumentCrud(session, current_user.project_id) - c_crud = CollectionCrud(session, current_user.project_id) - storage = get_cloud_storage(session=session, project_id=current_user.project_id) + d_crud = DocumentCrud(session, current_user.project_.id) + c_crud = CollectionCrud(session, current_user.project_.id) + storage = get_cloud_storage(session=session, project_id=current_user.project_.id) document = d_crud.read_one(doc_id) @@ -232,20 +238,21 @@ def permanent_delete_doc( "/{doc_id}", description=load_description("documents/info.md"), response_model=APIResponse[Union[DocumentPublic, TransformedDocumentPublic]], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def doc_info( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, doc_id: UUID = FastPath(description="Document to retrieve"), include_url: bool = Query( False, description="Include a signed URL to access the document" ), ): - crud = DocumentCrud(session, current_user.project_id) + crud = DocumentCrud(session, current_user.project_.id) document = crud.read_one(doc_id) storage = ( - get_cloud_storage(session=session, project_id=current_user.project_id) + get_cloud_storage(session=session, project_id=current_user.project_.id) if include_url else None ) diff --git a/backend/app/api/routes/evaluation.py b/backend/app/api/routes/evaluation.py index bc8ff2b03..9564a8d71 100644 --- a/backend/app/api/routes/evaluation.py +++ b/backend/app/api/routes/evaluation.py @@ -4,9 +4,10 @@ import re from pathlib import Path -from fastapi import APIRouter, Body, File, Form, HTTPException, Query, UploadFile +from fastapi import APIRouter, Body, File, Form, HTTPException, Query, UploadFile, Depends from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.core.cloud import get_cloud_storage from app.crud.assistants import get_assistant_by_id from app.crud.evaluations import ( @@ -113,6 +114,7 @@ def sanitize_dataset_name(name: str) -> str: "/evaluations/datasets", description=load_description("evaluation/upload_dataset.md"), response_model=APIResponse[DatasetUploadResponse], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) async def upload_dataset( _session: SessionDep, @@ -143,8 +145,8 @@ async def upload_dataset( logger.info( f"[upload_dataset] Uploading dataset | dataset={dataset_name} | " - f"duplication_factor={duplication_factor} | org_id={auth_context.organization.id} | " - f"project_id={auth_context.project.id}" + f"duplication_factor={duplication_factor} | org_id={auth_context.organization_.id} | " + f"project_id={auth_context.project_.id}" ) # Security validation: Check file extension @@ -234,7 +236,7 @@ async def upload_dataset( object_store_url = None try: storage = get_cloud_storage( - session=_session, project_id=auth_context.project.id + session=_session, project_id=auth_context.project_.id ) object_store_url = upload_csv_to_object_store( storage=storage, csv_content=csv_content, dataset_name=dataset_name @@ -260,8 +262,8 @@ async def upload_dataset( # Get Langfuse client langfuse = get_langfuse_client( session=_session, - org_id=auth_context.organization.id, - project_id=auth_context.project.id, + org_id=auth_context.organization_.id, + project_id=auth_context.project_.id, ) # Upload to Langfuse @@ -300,8 +302,8 @@ async def upload_dataset( dataset_metadata=metadata, object_store_url=object_store_url, langfuse_dataset_id=langfuse_dataset_id, - organization_id=auth_context.organization.id, - project_id=auth_context.project.id, + organization_id=auth_context.organization_.id, + project_id=auth_context.project_.id, ) logger.info( @@ -327,6 +329,7 @@ async def upload_dataset( "/evaluations/datasets", description=load_description("evaluation/list_datasets.md"), response_model=APIResponse[list[DatasetUploadResponse]], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def list_datasets_endpoint( _session: SessionDep, @@ -340,8 +343,8 @@ def list_datasets_endpoint( datasets = list_datasets( session=_session, - organization_id=auth_context.organization.id, - project_id=auth_context.project.id, + organization_id=auth_context.organization_.id, + project_id=auth_context.project_.id, limit=limit, offset=offset, ) @@ -355,6 +358,7 @@ def list_datasets_endpoint( "/evaluations/datasets/{dataset_id}", description=load_description("evaluation/get_dataset.md"), response_model=APIResponse[DatasetUploadResponse], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def get_dataset( dataset_id: int, @@ -363,15 +367,15 @@ def get_dataset( ) -> APIResponse[DatasetUploadResponse]: logger.info( f"[get_dataset] Fetching dataset | id={dataset_id} | " - f"org_id={auth_context.organization.id} | " - f"project_id={auth_context.project.id}" + f"org_id={auth_context.organization_.id} | " + f"project_id={auth_context.project_.id}" ) dataset = get_dataset_by_id( session=_session, dataset_id=dataset_id, - organization_id=auth_context.organization.id, - project_id=auth_context.project.id, + organization_id=auth_context.organization_.id, + project_id=auth_context.project_.id, ) if not dataset: @@ -386,6 +390,7 @@ def get_dataset( "/evaluations/datasets/{dataset_id}", description=load_description("evaluation/delete_dataset.md"), response_model=APIResponse[dict], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def delete_dataset( dataset_id: int, @@ -394,15 +399,15 @@ def delete_dataset( ) -> APIResponse[dict]: logger.info( f"[delete_dataset] Deleting dataset | id={dataset_id} | " - f"org_id={auth_context.organization.id} | " - f"project_id={auth_context.project.id}" + f"org_id={auth_context.organization_.id} | " + f"project_id={auth_context.project_.id}" ) success, message = delete_dataset_crud( session=_session, dataset_id=dataset_id, - organization_id=auth_context.organization.id, - project_id=auth_context.project.id, + organization_id=auth_context.organization_.id, + project_id=auth_context.project_.id, ) if not success: @@ -422,6 +427,7 @@ def delete_dataset( "/evaluations", description=load_description("evaluation/create_evaluation.md"), response_model=APIResponse[EvaluationRunPublic], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def evaluate( _session: SessionDep, @@ -439,7 +445,7 @@ def evaluate( logger.info( f"[evaluate] Starting evaluation | experiment_name={experiment_name} | " f"dataset_id={dataset_id} | " - f"org_id={auth_context.organization.id} | " + f"org_id={auth_context.organization_.id} | " f"assistant_id={assistant_id} | " f"config_keys={list(config.keys())}" ) @@ -448,8 +454,8 @@ def evaluate( dataset = get_dataset_by_id( session=_session, dataset_id=dataset_id, - organization_id=auth_context.organization.id, - project_id=auth_context.project.id, + organization_id=auth_context.organization_.id, + project_id=auth_context.project_.id, ) if not dataset: @@ -470,13 +476,13 @@ def evaluate( # Get API clients openai_client = get_openai_client( session=_session, - org_id=auth_context.organization.id, - project_id=auth_context.project.id, + org_id=auth_context.organization_.id, + project_id=auth_context.project_.id, ) langfuse = get_langfuse_client( session=_session, - org_id=auth_context.organization.id, - project_id=auth_context.project.id, + org_id=auth_context.organization_.id, + project_id=auth_context.project_.id, ) # Validate dataset has Langfuse ID (should have been set during dataset creation) @@ -493,7 +499,7 @@ def evaluate( assistant = get_assistant_by_id( session=_session, assistant_id=assistant_id, - project_id=auth_context.project.id, + project_id=auth_context.project_.id, ) if not assistant: @@ -544,8 +550,8 @@ def evaluate( dataset_name=dataset_name, dataset_id=dataset_id, config=config, - organization_id=auth_context.organization.id, - project_id=auth_context.project.id, + organization_id=auth_context.organization_.id, + project_id=auth_context.project_.id, ) # Start the batch evaluation @@ -579,6 +585,7 @@ def evaluate( "/evaluations", description=load_description("evaluation/list_evaluations.md"), response_model=APIResponse[list[EvaluationRunPublic]], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def list_evaluation_runs( _session: SessionDep, @@ -588,15 +595,15 @@ def list_evaluation_runs( ) -> APIResponse[list[EvaluationRunPublic]]: logger.info( f"[list_evaluation_runs] Listing evaluation runs | " - f"org_id={auth_context.organization.id} | " - f"project_id={auth_context.project.id} | limit={limit} | offset={offset}" + f"org_id={auth_context.organization_.id} | " + f"project_id={auth_context.project_.id} | limit={limit} | offset={offset}" ) return APIResponse.success_response( data=list_evaluation_runs_crud( session=_session, - organization_id=auth_context.organization.id, - project_id=auth_context.project.id, + organization_id=auth_context.organization_.id, + project_id=auth_context.project_.id, limit=limit, offset=offset, ) @@ -607,6 +614,7 @@ def list_evaluation_runs( "/evaluations/{evaluation_id}", description=load_description("evaluation/get_evaluation.md"), response_model=APIResponse[EvaluationRunPublic], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def get_evaluation_run_status( evaluation_id: int, @@ -632,8 +640,8 @@ def get_evaluation_run_status( logger.info( f"[get_evaluation_run_status] Fetching status for evaluation run | " f"evaluation_id={evaluation_id} | " - f"org_id={auth_context.organization.id} | " - f"project_id={auth_context.project.id} | " + f"org_id={auth_context.organization_.id} | " + f"project_id={auth_context.project_.id} | " f"get_trace_info={get_trace_info} | " f"resync_score={resync_score}" ) @@ -647,8 +655,8 @@ def get_evaluation_run_status( eval_run = get_evaluation_run_by_id( session=_session, evaluation_id=evaluation_id, - organization_id=auth_context.organization.id, - project_id=auth_context.project.id, + organization_id=auth_context.organization_.id, + project_id=auth_context.project_.id, ) if not eval_run: @@ -677,16 +685,16 @@ def get_evaluation_run_status( # Get Langfuse client (needs session for credentials lookup) langfuse = get_langfuse_client( session=_session, - org_id=auth_context.organization.id, - project_id=auth_context.project.id, + org_id=auth_context.organization_.id, + project_id=auth_context.project_.id, ) # Capture data needed for Langfuse fetch and DB update dataset_name = eval_run.dataset_name run_name = eval_run.run_name eval_run_id = eval_run.id - org_id = auth_context.organization.id - project_id = auth_context.project.id + org_id = auth_context.organization_.id + project_id = auth_context.project_.id # Session is no longer needed - slow Langfuse API calls happen here # without holding the DB connection diff --git a/backend/app/api/routes/fine_tuning.py b/backend/app/api/routes/fine_tuning.py index 252d42561..52761f238 100644 --- a/backend/app/api/routes/fine_tuning.py +++ b/backend/app/api/routes/fine_tuning.py @@ -6,7 +6,7 @@ import openai from sqlmodel import Session -from fastapi import APIRouter, HTTPException, BackgroundTasks, File, Form, UploadFile +from fastapi import APIRouter, HTTPException, BackgroundTasks, File, Form, UploadFile, Depends from app.models import ( FineTuningJobCreate, @@ -35,7 +35,8 @@ fetch_active_model_evals, ) from app.core.db import engine -from app.api.deps import CurrentUserOrgProject, SessionDep +from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.core.finetune.preprocessing import DataPreprocessor from app.api.routes.model_evaluation import run_model_evaluation @@ -57,11 +58,11 @@ def process_fine_tuning_job( job_id: int, ratio: float, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, request: FineTuningJobCreate, ): start_time = time.time() - project_id = current_user.project_id + project_id = current_user.project_.id fine_tune = None logger.info( @@ -72,12 +73,12 @@ def process_fine_tuning_job( fine_tune = fetch_by_id(session, job_id, project_id) client = get_openai_client( - session, current_user.organization_id, project_id + session, current_user.organization_.id, project_id ) storage = get_cloud_storage( - session=session, project_id=current_user.project_id + session=session, project_id=current_user.project_.id ) - document_crud = DocumentCrud(session, current_user.project_id) + document_crud = DocumentCrud(session, current_user.project_.id) document = document_crud.read_one(request.document_id) preprocessor = DataPreprocessor( document, storage, ratio, request.system_prompt @@ -184,10 +185,11 @@ def process_fine_tuning_job( "/fine_tune", description=load_description("fine_tuning/create.md"), response_model=APIResponse, + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) async def fine_tune_from_CSV( session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, background_tasks: BackgroundTasks, file: UploadFile = File(..., description="CSV file to use for fine-tuning"), base_model: str = Form(...), @@ -207,18 +209,18 @@ async def fine_tune_from_CSV( get_openai_client( # Used here only to validate the user's OpenAI key; # the actual client is re-initialized separately inside the background task session, - current_user.organization_id, - current_user.project_id, + current_user.organization_.id, + current_user.project_.id, ) # Upload the file to storage and create document # ToDo: create a helper function and then use it rather than doing things in router - storage = get_cloud_storage(session=session, project_id=current_user.project_id) + storage = get_cloud_storage(session=session, project_id=current_user.project_.id) document_id = uuid4() object_store_url = storage.put(file, Path(str(document_id))) # Create document in database - document_crud = DocumentCrud(session, current_user.project_id) + document_crud = DocumentCrud(session, current_user.project_.id) document = Document( id=document_id, fname=file.filename, @@ -241,8 +243,8 @@ async def fine_tune_from_CSV( session=session, request=request, split_ratio=ratio, - organization_id=current_user.organization_id, - project_id=current_user.project_id, + organization_id=current_user.organization_.id, + project_id=current_user.project_.id, ) results.append((job, created)) @@ -253,7 +255,7 @@ async def fine_tune_from_CSV( if not results: logger.error( - f"[fine_tune_from_CSV]All fine-tuning job creations failed for document_id={request.document_id}, project_id={current_user.project_id}" + f"[fine_tune_from_CSV]All fine-tuning job creations failed for document_id={request.document_id}, project_id={current_user.project_.id}" ) raise HTTPException( status_code=500, detail="Failed to create or fetch any fine-tuning jobs." @@ -286,17 +288,18 @@ async def fine_tune_from_CSV( "/{fine_tuning_id}/refresh", description=load_description("fine_tuning/retrieve.md"), response_model=APIResponse[FineTuningJobPublic], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def refresh_fine_tune_status( fine_tuning_id: int, background_tasks: BackgroundTasks, session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, ): - project_id = current_user.project_id + project_id = current_user.project_.id job = fetch_by_id(session, fine_tuning_id, project_id) - client = get_openai_client(session, current_user.organization_id, project_id) - storage = get_cloud_storage(session=session, project_id=current_user.project_id) + client = get_openai_client(session, current_user.organization_.id, project_id) + storage = get_cloud_storage(session=session, project_id=current_user.project_.id) if job.provider_job_id is not None: try: @@ -358,7 +361,7 @@ def refresh_fine_tune_status( session=session, request=ModelEvaluationBase(fine_tuning_id=fine_tuning_id), project_id=project_id, - organization_id=current_user.organization_id, + organization_id=current_user.organization_.id, status=ModelEvaluationStatus.pending, ) @@ -395,12 +398,13 @@ def refresh_fine_tune_status( "/{document_id}", description="Retrieves all fine-tuning jobs associated with the given document ID for the current project", response_model=APIResponse[list[FineTuningJobPublic]], + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def retrieve_jobs_by_document( - document_id: UUID, session: SessionDep, current_user: CurrentUserOrgProject + document_id: UUID, session: SessionDep, current_user: AuthContextDep ): - storage = get_cloud_storage(session=session, project_id=current_user.project_id) - project_id = current_user.project_id + storage = get_cloud_storage(session=session, project_id=current_user.project_.id) + project_id = current_user.project_.id jobs = fetch_by_document_id(session, document_id, project_id) if not jobs: logger.warning( diff --git a/backend/app/api/routes/model_evaluation.py b/backend/app/api/routes/model_evaluation.py index 2a14c881d..6425666a5 100644 --- a/backend/app/api/routes/model_evaluation.py +++ b/backend/app/api/routes/model_evaluation.py @@ -2,7 +2,7 @@ import time from uuid import UUID -from fastapi import APIRouter, HTTPException, BackgroundTasks +from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends from sqlmodel import Session from app.crud import ( @@ -24,7 +24,8 @@ from app.core.cloud import get_cloud_storage from app.core.finetune.evaluation import ModelEvaluator from app.utils import get_openai_client, APIResponse, load_description -from app.api.deps import CurrentUserOrgProject, SessionDep +from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission logger = logging.getLogger(__name__) @@ -47,24 +48,24 @@ def attach_prediction_file_url(model_obj, storage): def run_model_evaluation( eval_id: int, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, ): start_time = time.time() logger.info( - f"[run_model_evaluation] Starting | eval_id={eval_id}, project_id={current_user.project_id}" + f"[run_model_evaluation] Starting | eval_id={eval_id}, project_id={current_user.project_.id}" ) with Session(engine) as db: client = get_openai_client( - db, current_user.organization_id, current_user.project_id + db, current_user.organization_.id, current_user.project_.id ) - storage = get_cloud_storage(session=db, project_id=current_user.project_id) + storage = get_cloud_storage(session=db, project_id=current_user.project_.id) try: model_eval = update_model_eval( session=db, eval_id=eval_id, - project_id=current_user.project_id, + project_id=current_user.project_.id, update=ModelEvaluationUpdate(status=ModelEvaluationStatus.running), ) @@ -80,7 +81,7 @@ def run_model_evaluation( update_model_eval( session=db, eval_id=eval_id, - project_id=current_user.project_id, + project_id=current_user.project_.id, update=ModelEvaluationUpdate( score=result["evaluation_score"], prediction_data_s3_object=result["prediction_data_s3_object"], @@ -90,19 +91,19 @@ def run_model_evaluation( elapsed = time.time() - start_time logger.info( - f"[run_model_evaluation] Completed | eval_id={eval_id}, project_id={current_user.project_id}, elapsed={elapsed:.2f}s" + f"[run_model_evaluation] Completed | eval_id={eval_id}, project_id={current_user.project_.id}, elapsed={elapsed:.2f}s" ) except Exception as e: error_msg = str(e) logger.error( - f"[run_model_evaluation] Failed | eval_id={eval_id}, project_id={current_user.project_id}: {e}" + f"[run_model_evaluation] Failed | eval_id={eval_id}, project_id={current_user.project_.id}: {e}" ) db.rollback() update_model_eval( session=db, eval_id=eval_id, - project_id=current_user.project_id, + project_id=current_user.project_.id, update=ModelEvaluationUpdate( status=ModelEvaluationStatus.failed, error_message="failed during background job processing:" @@ -115,12 +116,13 @@ def run_model_evaluation( "/evaluate_models/", response_model=APIResponse, description=load_description("model_evaluation/evaluate.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def evaluate_models( request: ModelEvaluationCreate, background_tasks: BackgroundTasks, session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, ): """ Start evaluations for one or more fine-tuning jobs. @@ -136,27 +138,27 @@ def evaluate_models( APIResponse with the created/active evaluation records and a success message. """ client = get_openai_client( - session, current_user.organization_id, current_user.project_id + session, current_user.organization_.id, current_user.project_.id ) # keeping this here for checking if the user's validated OpenAI key is present or not, # even though the client will be initialized separately inside the background task if not request.fine_tuning_ids: logger.error( - f"[evaluate_model] No fine tuning IDs provided | project_id:{current_user.project_id}" + f"[evaluate_model] No fine tuning IDs provided | project_id:{current_user.project_.id}" ) raise HTTPException(status_code=400, detail="No fine-tuned job IDs provided") evaluations: list[ModelEvaluationPublic] = [] for job_id in request.fine_tuning_ids: - fine_tuning_job = fetch_by_id(session, job_id, current_user.project_id) + fine_tuning_job = fetch_by_id(session, job_id, current_user.project_.id) active_evaluations = fetch_active_model_evals( - session, job_id, current_user.project_id + session, job_id, current_user.project_.id ) if active_evaluations: logger.info( - f"[evaluate_model] Skipping creation for {job_id}. Active evaluation exists, project_id:{current_user.project_id}" + f"[evaluate_model] Skipping creation for {job_id}. Active evaluation exists, project_id:{current_user.project_.id}" ) evaluations.extend( ModelEvaluationPublic.model_validate(ev) for ev in active_evaluations @@ -166,15 +168,15 @@ def evaluate_models( model_eval = create_model_evaluation( session=session, request=ModelEvaluationBase(fine_tuning_id=fine_tuning_job.id), - project_id=current_user.project_id, - organization_id=current_user.organization_id, + project_id=current_user.project_.id, + organization_id=current_user.organization_.id, status=ModelEvaluationStatus.pending, ) evaluations.append(ModelEvaluationPublic.model_validate(model_eval)) logger.info( - f"[evaluate_model] Created evaluation for fine_tuning_id {job_id} with eval ID={model_eval.id}, project_id:{current_user.project_id}" + f"[evaluate_model] Created evaluation for fine_tuning_id {job_id} with eval ID={model_eval.id}, project_id:{current_user.project_.id}" ) background_tasks.add_task(run_model_evaluation, model_eval.id, current_user) @@ -200,11 +202,12 @@ def evaluate_models( response_model=APIResponse[ModelEvaluationPublic], response_model_exclude_none=True, description=load_description("model_evaluation/get_top_model.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def get_top_model_by_doc_id( document_id: UUID, session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, ): """ Return the top model trained on the given document_id, ranked by @@ -212,11 +215,11 @@ def get_top_model_by_doc_id( """ logger.info( f"[get_top_model_by_doc_id] Fetching top model for document_id={document_id}, " - f"project_id={current_user.project_id}" + f"project_id={current_user.project_.id}" ) - top_model = fetch_top_model_by_doc_id(session, document_id, current_user.project_id) - storage = get_cloud_storage(session=session, project_id=current_user.project_id) + top_model = fetch_top_model_by_doc_id(session, document_id, current_user.project_.id) + storage = get_cloud_storage(session=session, project_id=current_user.project_.id) top_model = attach_prediction_file_url(top_model, storage) @@ -228,22 +231,23 @@ def get_top_model_by_doc_id( response_model=APIResponse[list[ModelEvaluationPublic]], response_model_exclude_none=True, description=load_description("model_evaluation/list_by_document.md"), + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def get_evaluations_by_doc_id( document_id: UUID, session: SessionDep, - current_user: CurrentUserOrgProject, + current_user: AuthContextDep, ): """ Return all model evaluations for the given document_id within the current project. """ logger.info( f"[get_evaluations_by_doc_id] Fetching evaluations for document_id={document_id}, " - f"project_id={current_user.project_id}" + f"project_id={current_user.project_.id}" ) - evaluations = fetch_eval_by_doc_id(session, document_id, current_user.project_id) - storage = get_cloud_storage(session=session, project_id=current_user.project_id) + evaluations = fetch_eval_by_doc_id(session, document_id, current_user.project_.id) + storage = get_cloud_storage(session=session, project_id=current_user.project_.id) updated_evaluations = [ attach_prediction_file_url(ev, storage) for ev in evaluations From e477df43951a76ea87685a24c5f5692203557393 Mon Sep 17 00:00:00 2001 From: Aviraj Gour <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:40:44 +0530 Subject: [PATCH 5/9] Refactor dependency imports and remove unused parameters across multiple files --- backend/app/api/deps.py | 109 --------------------- backend/app/api/routes/documents.py | 1 - backend/app/api/routes/evaluation.py | 11 ++- backend/app/api/routes/fine_tuning.py | 10 +- backend/app/api/routes/login.py | 9 +- backend/app/api/routes/model_evaluation.py | 4 +- backend/app/api/routes/users.py | 39 ++++---- backend/app/core/cloud/storage.py | 1 - backend/app/services/doctransform/job.py | 7 +- backend/app/services/documents/helpers.py | 3 +- 10 files changed, 51 insertions(+), 143 deletions(-) diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 73cb77427..9f2c81a62 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -17,8 +17,6 @@ AuthContext, TokenPayload, User, - UserOrganization, - UserProjectOrg, ) @@ -37,113 +35,6 @@ def get_db() -> Generator[Session, None, None]: TokenDep = Annotated[str, Depends(reusable_oauth2)] -def get_current_user( - session: SessionDep, - token: TokenDep, - api_key: Annotated[str, Depends(api_key_header)], -) -> User: - """Authenticate user via API Key first, fallback to JWT token. Returns only User.""" - - if api_key: - api_key_record = api_key_manager.verify(session, api_key) - if not api_key_record: - raise HTTPException(status_code=401, detail="Invalid API Key") - - if not api_key_record.user.is_active: - raise HTTPException(status_code=403, detail="Inactive user") - - return api_key_record.user # Return only User object - - elif token: - try: - payload = jwt.decode( - token, settings.SECRET_KEY, algorithms=[security.ALGORITHM] - ) - token_data = TokenPayload(**payload) - except (InvalidTokenError, ValidationError): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials", - ) - - user = session.get(User, token_data.sub) - if not user: - raise HTTPException(status_code=404, detail="User not found") - if not user.is_active: - raise HTTPException(status_code=403, detail="Inactive user") - - return user # Return only User object - - raise HTTPException(status_code=401, detail="Invalid Authorization format") - - -CurrentUser = Annotated[User, Depends(get_current_user)] - - -def get_current_user_org( - current_user: CurrentUser, session: SessionDep, request: Request -) -> UserOrganization: - """Extend `User` with organization_id if available, otherwise return UserOrganization without it.""" - - organization_id = None - api_key = request.headers.get("X-API-KEY") - if api_key: - api_key_record = api_key_manager.verify(session, api_key) - if api_key_record: - validate_organization(session, api_key_record.organization.id) - organization_id = api_key_record.organization.id - - return UserOrganization( - **current_user.model_dump(), organization_id=organization_id - ) - - -CurrentUserOrg = Annotated[UserOrganization, Depends(get_current_user_org)] - - -def get_current_user_org_project( - current_user: CurrentUser, session: SessionDep, request: Request -) -> UserProjectOrg: - api_key = request.headers.get("X-API-KEY") - organization_id = None - project_id = None - - if api_key: - api_key_record = api_key_manager.verify(session, api_key) - if api_key_record: - validate_organization(session, api_key_record.organization.id) - organization_id = api_key_record.organization.id - project_id = api_key_record.project.id - - else: - raise HTTPException(status_code=401, detail="Invalid API Key") - - return UserProjectOrg( - **current_user.model_dump(), - organization_id=organization_id, - project_id=project_id, - ) - - -CurrentUserOrgProject = Annotated[UserProjectOrg, Depends(get_current_user_org_project)] - - -def get_current_active_superuser(current_user: CurrentUser) -> User: - if not current_user.is_superuser: - raise HTTPException( - status_code=403, detail="The user doesn't have enough privileges" - ) - return current_user - - -def get_current_active_superuser_org(current_user: CurrentUserOrg) -> User: - if not current_user.is_superuser: - raise HTTPException( - status_code=403, detail="The user doesn't have enough privileges" - ) - return current_user - - def get_auth_context( session: SessionDep, token: TokenDep, diff --git a/backend/app/api/routes/documents.py b/backend/app/api/routes/documents.py index 706f0f269..16fdcfc64 100644 --- a/backend/app/api/routes/documents.py +++ b/backend/app/api/routes/documents.py @@ -145,7 +145,6 @@ async def upload_doc( job_info: TransformationJobInfo | None = schedule_transformation( session=session, project_id=current_user.project_.id, - current_user=current_user, source_format=source_format, target_format=target_format, actual_transformer=actual_transformer, diff --git a/backend/app/api/routes/evaluation.py b/backend/app/api/routes/evaluation.py index 9564a8d71..6175476db 100644 --- a/backend/app/api/routes/evaluation.py +++ b/backend/app/api/routes/evaluation.py @@ -4,7 +4,16 @@ import re from pathlib import Path -from fastapi import APIRouter, Body, File, Form, HTTPException, Query, UploadFile, Depends +from fastapi import ( + APIRouter, + Body, + File, + Form, + HTTPException, + Query, + UploadFile, + Depends, +) from app.api.deps import AuthContextDep, SessionDep from app.api.permissions import Permission, require_permission diff --git a/backend/app/api/routes/fine_tuning.py b/backend/app/api/routes/fine_tuning.py index 52761f238..69f8951df 100644 --- a/backend/app/api/routes/fine_tuning.py +++ b/backend/app/api/routes/fine_tuning.py @@ -6,7 +6,15 @@ import openai from sqlmodel import Session -from fastapi import APIRouter, HTTPException, BackgroundTasks, File, Form, UploadFile, Depends +from fastapi import ( + APIRouter, + HTTPException, + BackgroundTasks, + File, + Form, + UploadFile, + Depends, +) from app.models import ( FineTuningJobCreate, diff --git a/backend/app/api/routes/login.py b/backend/app/api/routes/login.py index 0ca6b8f61..704a5e8d7 100644 --- a/backend/app/api/routes/login.py +++ b/backend/app/api/routes/login.py @@ -5,7 +5,8 @@ from fastapi.responses import HTMLResponse from fastapi.security import OAuth2PasswordRequestForm -from app.api.deps import CurrentUser, SessionDep, get_current_active_superuser +from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.core import security from app.core.config import settings from app.core.security import get_password_hash @@ -51,11 +52,11 @@ def login_access_token( @router.post("/login/test-token", response_model=UserPublic, include_in_schema=False) -def test_token(current_user: CurrentUser) -> Any: +def test_token(current_user: AuthContextDep) -> Any: """ Test access token """ - return current_user + return current_user.user @router.post("/password-recovery/{email}", include_in_schema=False) @@ -107,7 +108,7 @@ def reset_password(session: SessionDep, body: NewPassword) -> Message: @router.post( "/password-recovery-html-content/{email}", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_class=HTMLResponse, include_in_schema=False, ) diff --git a/backend/app/api/routes/model_evaluation.py b/backend/app/api/routes/model_evaluation.py index 6425666a5..c22c78508 100644 --- a/backend/app/api/routes/model_evaluation.py +++ b/backend/app/api/routes/model_evaluation.py @@ -218,7 +218,9 @@ def get_top_model_by_doc_id( f"project_id={current_user.project_.id}" ) - top_model = fetch_top_model_by_doc_id(session, document_id, current_user.project_.id) + top_model = fetch_top_model_by_doc_id( + session, document_id, current_user.project_.id + ) storage = get_cloud_storage(session=session, project_id=current_user.project_.id) top_model = attach_prediction_file_url(top_model, storage) diff --git a/backend/app/api/routes/users.py b/backend/app/api/routes/users.py index 55a017409..ba13a6c1c 100644 --- a/backend/app/api/routes/users.py +++ b/backend/app/api/routes/users.py @@ -5,10 +5,10 @@ from sqlmodel import func, select from app.api.deps import ( - CurrentUser, + AuthContextDep, SessionDep, - get_current_active_superuser, ) +from app.api.permissions import Permission, require_permission from app.core.config import settings from app.core.security import get_password_hash, verify_password from app.crud import create_user, get_user_by_email, update_user @@ -32,7 +32,7 @@ @router.get( "/", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=UsersPublic, include_in_schema=False, ) @@ -44,7 +44,7 @@ def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any: @router.post( "/", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=UserPublic, include_in_schema=False, ) @@ -74,8 +74,9 @@ def create_user_endpoint(*, session: SessionDep, user_in: UserCreate) -> Any: @router.patch("/me", response_model=UserPublic) def update_user_me( - *, session: SessionDep, user_in: UserUpdateMe, current_user: CurrentUser + *, session: SessionDep, user_in: UserUpdateMe, current_user_dep: AuthContextDep ) -> Any: + current_user = current_user_dep.user if user_in.email: existing_user = get_user_by_email(session=session, email=user_in.email) if existing_user and existing_user.id != current_user.id: @@ -96,8 +97,9 @@ def update_user_me( @router.patch("/me/password", response_model=Message) def update_password_me( - *, session: SessionDep, body: UpdatePassword, current_user: CurrentUser + *, session: SessionDep, body: UpdatePassword, current_user_dep: AuthContextDep ) -> Any: + current_user = current_user_dep.user if not verify_password(body.current_password, current_user.hashed_password): raise HTTPException(status_code=400, detail="Incorrect password") @@ -115,12 +117,13 @@ def update_password_me( @router.get("/me", response_model=UserPublic) -def read_user_me(current_user: CurrentUser) -> Any: - return current_user +def read_user_me(current_user_dep: AuthContextDep) -> Any: + return current_user_dep.user @router.delete("/me", response_model=Message) -def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any: +def delete_user_me(session: SessionDep, current_user_dep: AuthContextDep) -> Any: + current_user = current_user_dep.user if current_user.is_superuser: logger.error( f"[delete_user_me] Attempting to delete superuser account by itself | user_id: {current_user.id}" @@ -136,7 +139,7 @@ def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any: @router.post( "/signup", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=UserPublic, ) def register_user(session: SessionDep, user_in: UserRegister) -> Any: @@ -158,13 +161,13 @@ def register_user(session: SessionDep, user_in: UserRegister) -> Any: @router.get("/{user_id}", response_model=UserPublic, include_in_schema=False) def read_user_by_id( - user_id: int, session: SessionDep, current_user: CurrentUser + user_id: int, session: SessionDep, current_user: AuthContextDep ) -> Any: user = session.get(User, user_id) - if user == current_user: + if user == current_user.user: return user - if not current_user.is_superuser: + if not current_user.user.is_superuser: raise HTTPException( status_code=403, detail="The user doesn't have enough privileges", @@ -175,7 +178,7 @@ def read_user_by_id( @router.patch( "/{user_id}", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], response_model=UserPublic, include_in_schema=False, ) @@ -208,20 +211,20 @@ def update_user_endpoint( @router.delete( "/{user_id}", - dependencies=[Depends(get_current_active_superuser)], + dependencies=[Depends(require_permission(Permission.SUPERUSER))], include_in_schema=False, ) def delete_user( - session: SessionDep, current_user: CurrentUser, user_id: int + session: SessionDep, current_user: AuthContextDep, user_id: int ) -> Message: user = session.get(User, user_id) if not user: logger.error(f"[delete_user] User not found | user_id: {user_id}") raise HTTPException(status_code=404, detail="User not found") - if user == current_user: + if user == current_user.user: logger.error( - f"[delete_user] Attempting to delete self by superuser | user_id: {current_user.id}" + f"[delete_user] Attempting to delete self by superuser | user_id: {current_user.user.id}" ) raise HTTPException( status_code=403, detail="Super users are not allowed to delete themselves" diff --git a/backend/app/core/cloud/storage.py b/backend/app/core/cloud/storage.py index 95c7f0ddf..a3247b74b 100644 --- a/backend/app/core/cloud/storage.py +++ b/backend/app/core/cloud/storage.py @@ -14,7 +14,6 @@ from botocore.response import StreamingBody from app.crud import get_project_by_id -from app.models import UserProjectOrg from app.core.config import settings from app.utils import mask_string diff --git a/backend/app/services/doctransform/job.py b/backend/app/services/doctransform/job.py index 8245b213b..3018ffc4b 100644 --- a/backend/app/services/doctransform/job.py +++ b/backend/app/services/doctransform/job.py @@ -22,7 +22,6 @@ DocTransformationJob, ) from app.core.cloud import get_cloud_storage -from app.api.deps import CurrentUserOrgProject from app.celery.utils import start_low_priority_job from app.utils import send_callback, APIResponse from app.services.doctransform.registry import convert_document, FORMAT_TO_EXTENSION @@ -33,19 +32,17 @@ def start_job( db: Session, - current_user: CurrentUserOrgProject, + project_id: int, job_id: UUID, transformer_name: str, target_format: str, callback_url: str | None, ) -> str: trace_id = correlation_id.get() or "N/A" - job_crud = DocTransformationJobCrud(db, project_id=current_user.project_id) + job_crud = DocTransformationJobCrud(db, project_id=project_id) job_crud.update(job_id, DocTransformJobUpdate(trace_id=trace_id)) job = job_crud.read_one(job_id) - project_id = current_user.project_id - task_id = start_low_priority_job( function_path="app.services.doctransform.job.execute_job", project_id=project_id, diff --git a/backend/app/services/documents/helpers.py b/backend/app/services/documents/helpers.py index 7319830bf..cd941eb55 100644 --- a/backend/app/services/documents/helpers.py +++ b/backend/app/services/documents/helpers.py @@ -70,7 +70,6 @@ def schedule_transformation( *, session, project_id: int, - current_user, source_format: str, target_format: str | None, actual_transformer: str | None, @@ -92,7 +91,7 @@ def schedule_transformation( transformation_job_id = transformation_job.start_job( db=session, job_id=job.id, - current_user=current_user, + project_id=project_id, transformer_name=actual_transformer, target_format=target_format, callback_url=callback_url, From ac15d8c216ccd6a5ec2a411117f3d0f5f55623b3 Mon Sep 17 00:00:00 2001 From: Aviraj Gour <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:56:02 +0530 Subject: [PATCH 6/9] Refactor user model by removing UserOrganization and UserProjectOrg classes; update tests to use AuthContext for user-related operations --- backend/app/models/__init__.py | 2 -- backend/app/models/user.py | 9 ------ backend/app/tests/api/routes/test_users.py | 2 +- .../doctransformer/test_job/conftest.py | 20 ++++++------- .../test_job/test_integration.py | 10 +------ .../doctransformer/test_job/test_start_job.py | 30 +++++++++---------- 6 files changed, 26 insertions(+), 47 deletions(-) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 9a3518251..ac7e89d6c 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -142,8 +142,6 @@ NewPassword, User, UserCreate, - UserOrganization, - UserProjectOrg, UserPublic, UserRegister, UserUpdate, diff --git a/backend/app/models/user.py b/backend/app/models/user.py index b3d309741..f38aafc2a 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -75,15 +75,6 @@ class User(UserBase, table=True): ) -class UserOrganization(UserBase): - id: int - organization_id: int | None - - -class UserProjectOrg(UserOrganization): - project_id: int - - # Properties to return via API, id is always required class UserPublic(UserBase): id: int diff --git a/backend/app/tests/api/routes/test_users.py b/backend/app/tests/api/routes/test_users.py index 4b7c3fdee..d8d936e46 100644 --- a/backend/app/tests/api/routes/test_users.py +++ b/backend/app/tests/api/routes/test_users.py @@ -470,4 +470,4 @@ def test_delete_user_without_privileges( headers=normal_user_token_headers, ) assert r.status_code == 403 - assert r.json()["error"] == "The user doesn't have enough privileges" + assert r.json()["error"] == "Insufficient permissions - require superuser access." diff --git a/backend/app/tests/services/doctransformer/test_job/conftest.py b/backend/app/tests/services/doctransformer/test_job/conftest.py index edba7ec9d..7f85742f0 100644 --- a/backend/app/tests/services/doctransformer/test_job/conftest.py +++ b/backend/app/tests/services/doctransformer/test_job/conftest.py @@ -13,7 +13,7 @@ from app.crud import get_project_by_id from app.services.doctransform import job from app.core.config import settings -from app.models import Document, Project, UserProjectOrg +from app.models import Document, Project, AuthContext from app.tests.utils.document import DocumentStore from app.tests.utils.auth import TestAuthContext @@ -68,14 +68,12 @@ def fast_execute_job_func( @pytest.fixture -def current_user(db: Session, user_api_key: TestAuthContext) -> UserProjectOrg: +def current_user(db: Session, user_api_key: TestAuthContext) -> AuthContext: """Create a test user for testing.""" - api_key = user_api_key - user = api_key.user - return UserProjectOrg( - **user.model_dump(), - project_id=api_key.project_id, - organization_id=api_key.organization_id + return AuthContext( + user=user_api_key.user, + organization=user_api_key.organization, + project=user_api_key.project ) @@ -87,9 +85,9 @@ def background_tasks() -> BackgroundTasks: @pytest.fixture def test_document( - db: Session, current_user: UserProjectOrg + db: Session, current_user: AuthContext ) -> Tuple[Document, Project]: """Create a test document for the current user's project.""" - store = DocumentStore(db, current_user.project_id) - project = get_project_by_id(session=db, project_id=current_user.project_id) + store = DocumentStore(db, current_user.project.id) + project = get_project_by_id(session=db, project_id=current_user.project.id) return store.put(), project diff --git a/backend/app/tests/services/doctransformer/test_job/test_integration.py b/backend/app/tests/services/doctransformer/test_job/test_integration.py index 3282b772c..936bf6efb 100644 --- a/backend/app/tests/services/doctransformer/test_job/test_integration.py +++ b/backend/app/tests/services/doctransformer/test_job/test_integration.py @@ -15,7 +15,6 @@ Document, Project, TransformationStatus, - UserProjectOrg, DocTransformJobCreate, ) from app.tests.services.doctransformer.test_job.utils import ( @@ -40,13 +39,6 @@ def test_execute_job_end_to_end_workflow( job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(DocTransformJobCreate(source_document_id=document.id)) - current_user = UserProjectOrg( - id=1, - email="test@example.com", - project_id=project.id, - organization_id=project.organization_id, - ) - with patch( "app.services.doctransform.job.start_low_priority_job", return_value="fake-task-id", @@ -59,7 +51,7 @@ def test_execute_job_end_to_end_workflow( returned_job_id = start_job( db=db, - current_user=current_user, + project_id=project.id, job_id=job.id, transformer_name="test", target_format="markdown", diff --git a/backend/app/tests/services/doctransformer/test_job/test_start_job.py b/backend/app/tests/services/doctransformer/test_job/test_start_job.py index 60e3dadee..1922e6730 100644 --- a/backend/app/tests/services/doctransformer/test_job/test_start_job.py +++ b/backend/app/tests/services/doctransformer/test_job/test_start_job.py @@ -16,7 +16,7 @@ DocTransformationJob, Project, TransformationStatus, - UserProjectOrg, + AuthContext, DocTransformJobCreate, ) from app.tests.services.doctransformer.test_job.utils import ( @@ -36,13 +36,13 @@ def _create_job(self, db: Session, project_id: int, source_document_id): def test_start_job_success( self, db: Session, - current_user: UserProjectOrg, + current_user: AuthContext, test_document: tuple[Document, Project], ) -> None: """start_job should enqueue execute_job with correct kwargs and return the same job id.""" document, _project = test_document - job = self._create_job(db, current_user.project_id, document.id) + job = self._create_job(db, current_user.project.id, document.id) with patch( "app.services.doctransform.job.start_low_priority_job" @@ -51,7 +51,7 @@ def test_start_job_success( returned_job_id = start_job( db=db, - current_user=current_user, + project_id=current_user.project.id, job_id=job.id, transformer_name="test-transformer", target_format="markdown", @@ -70,7 +70,7 @@ def test_start_job_success( mock_schedule.assert_called_once() kwargs = mock_schedule.call_args.kwargs assert kwargs["function_path"] == "app.services.doctransform.job.execute_job" - assert kwargs["project_id"] == current_user.project_id + assert kwargs["project_id"] == current_user.project.id assert kwargs["job_id"] == str(job.id) assert kwargs["source_document_id"] == str(job.source_document_id) assert kwargs["transformer_name"] == "test-transformer" @@ -80,7 +80,7 @@ def test_start_job_success( def test_start_job_with_nonexistent_document( self, db: Session, - current_user: UserProjectOrg, + current_user: AuthContext, ) -> None: """ Previously: start_job validated document and raised 404. @@ -95,7 +95,7 @@ def test_start_job_with_nonexistent_document( mock_schedule.return_value = "fake-task-id" start_job( db=db, - current_user=current_user, + project_id=current_user.project.id, job_id=nonexistent_job_id, transformer_name="test-transformer", target_format="markdown", @@ -105,7 +105,7 @@ def test_start_job_with_nonexistent_document( def test_start_job_with_different_formats( self, db: Session, - current_user: UserProjectOrg, + current_user: AuthContext, test_document: tuple[Document, Project], monkeypatch, ) -> None: @@ -121,11 +121,11 @@ def test_start_job_with_different_formats( mock_schedule.return_value = "fake-task-id" for target_format in formats: - job = self._create_job(db, current_user.project_id, document.id) + job = self._create_job(db, current_user.project.id, document.id) returned_job_id = start_job( db=db, - current_user=current_user, + project_id=current_user.project.id, job_id=job.id, transformer_name="test", target_format=target_format, @@ -144,7 +144,7 @@ def test_start_job_with_different_formats( kwargs["function_path"] == "app.services.doctransform.job.execute_job" ) - assert kwargs["project_id"] == current_user.project_id + assert kwargs["project_id"] == current_user.project.id assert kwargs["job_id"] == str(job.id) assert kwargs["source_document_id"] == str(job.source_document_id) assert kwargs["transformer_name"] == "test" @@ -154,7 +154,7 @@ def test_start_job_with_different_formats( def test_start_job_with_different_transformers( self, db: Session, - current_user: UserProjectOrg, + current_user: AuthContext, test_document: tuple[Document, Project], transformer_name: str, monkeypatch, @@ -163,7 +163,7 @@ def test_start_job_with_different_transformers( monkeypatch.setitem(TRANSFORMERS, "test", MockTestTransformer) document, _ = test_document - job = self._create_job(db, current_user.project_id, document.id) + job = self._create_job(db, current_user.project.id, document.id) with patch( "app.services.doctransform.job.start_low_priority_job" @@ -172,7 +172,7 @@ def test_start_job_with_different_transformers( returned_job_id = start_job( db=db, - current_user=current_user, + project_id=current_user.project.id, job_id=job.id, transformer_name=transformer_name, target_format="markdown", @@ -187,7 +187,7 @@ def test_start_job_with_different_transformers( assert kwargs["transformer_name"] == transformer_name assert kwargs["target_format"] == "markdown" assert kwargs["function_path"] == "app.services.doctransform.job.execute_job" - assert kwargs["project_id"] == current_user.project_id + assert kwargs["project_id"] == current_user.project.id assert kwargs["job_id"] == str(job.id) assert kwargs["source_document_id"] == str(job.source_document_id) assert returned_job_id == job.id From 1959efc7d226cd2e52234c7f589585fad93ae5c6 Mon Sep 17 00:00:00 2001 From: Aviraj Gour <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:03:24 +0530 Subject: [PATCH 7/9] precommit --- .../app/tests/services/doctransformer/test_job/conftest.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/app/tests/services/doctransformer/test_job/conftest.py b/backend/app/tests/services/doctransformer/test_job/conftest.py index 7f85742f0..8787db17a 100644 --- a/backend/app/tests/services/doctransformer/test_job/conftest.py +++ b/backend/app/tests/services/doctransformer/test_job/conftest.py @@ -73,7 +73,7 @@ def current_user(db: Session, user_api_key: TestAuthContext) -> AuthContext: return AuthContext( user=user_api_key.user, organization=user_api_key.organization, - project=user_api_key.project + project=user_api_key.project, ) @@ -84,9 +84,7 @@ def background_tasks() -> BackgroundTasks: @pytest.fixture -def test_document( - db: Session, current_user: AuthContext -) -> Tuple[Document, Project]: +def test_document(db: Session, current_user: AuthContext) -> Tuple[Document, Project]: """Create a test document for the current user's project.""" store = DocumentStore(db, current_user.project.id) project = get_project_by_id(session=db, project_id=current_user.project.id) From 06c9d93fc80ed74b59e916afdc1f0f1b7c85efaf Mon Sep 17 00:00:00 2001 From: Aviraj Gour <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:11:29 +0530 Subject: [PATCH 8/9] require project in llm call --- backend/app/api/routes/llm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/llm.py b/backend/app/api/routes/llm.py index 239f1a5ba..93b542216 100644 --- a/backend/app/api/routes/llm.py +++ b/backend/app/api/routes/llm.py @@ -1,8 +1,9 @@ import logging -from fastapi import APIRouter +from fastapi import APIRouter, Depends from app.api.deps import AuthContextDep, SessionDep +from app.api.permissions import Permission, require_permission from app.models import LLMCallRequest, LLMCallResponse, Message from app.services.llm.jobs import start_job from app.utils import APIResponse, validate_callback_url, load_description @@ -35,6 +36,7 @@ def llm_callback_notification(body: APIResponse[LLMCallResponse]): description=load_description("llm/llm_call.md"), response_model=APIResponse[Message], callbacks=llm_callback_router.routes, + dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))], ) def llm_call( _current_user: AuthContextDep, _session: SessionDep, request: LLMCallRequest From e1ce64d062175d6b6325e58aa3cdce51ef2c51dc Mon Sep 17 00:00:00 2001 From: Aviraj Gour <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:26:12 +0530 Subject: [PATCH 9/9] fix: update project attribute reference in CRUD operations --- backend/app/api/routes/api_keys.py | 4 ++-- backend/app/api/routes/config/config.py | 10 +++++----- backend/app/api/routes/config/version.py | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/app/api/routes/api_keys.py b/backend/app/api/routes/api_keys.py index bac5a3463..723eecc84 100644 --- a/backend/app/api/routes/api_keys.py +++ b/backend/app/api/routes/api_keys.py @@ -50,7 +50,7 @@ def list_api_keys_route( skip: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(100, ge=1, le=100, description="Maximum records to return"), ): - crud = APIKeyCrud(session, current_user.project.id) + crud = APIKeyCrud(session, current_user.project_.id) api_keys = crud.read_all(skip=skip, limit=limit) return APIResponse.success_response(api_keys) @@ -67,7 +67,7 @@ def delete_api_key_route( current_user: AuthContextDep, session: SessionDep, ): - api_key_crud = APIKeyCrud(session=session, project_id=current_user.project.id) + api_key_crud = APIKeyCrud(session=session, project_id=current_user.project_.id) api_key_crud.delete(key_id=key_id) return APIResponse.success_response(Message(message="API Key deleted successfully")) diff --git a/backend/app/api/routes/config/config.py b/backend/app/api/routes/config/config.py index 18c3ca84e..6d2629442 100644 --- a/backend/app/api/routes/config/config.py +++ b/backend/app/api/routes/config/config.py @@ -33,7 +33,7 @@ def create_config( """ create new config along with initial version """ - config_crud = ConfigCrud(session=session, project_id=current_user.project.id) + config_crud = ConfigCrud(session=session, project_id=current_user.project_.id) config, version = config_crud.create_or_raise(config_create) response = ConfigWithVersion(**config.model_dump(), version=version) @@ -60,7 +60,7 @@ def list_configs( List all configurations for the current project. Ordered by updated_at in descending order. """ - config_crud = ConfigCrud(session=session, project_id=current_user.project.id) + config_crud = ConfigCrud(session=session, project_id=current_user.project_.id) configs = config_crud.read_all(skip=skip, limit=limit) return APIResponse.success_response( data=configs, @@ -82,7 +82,7 @@ def get_config( """ Get a specific configuration by its ID. """ - config_crud = ConfigCrud(session=session, project_id=current_user.project.id) + config_crud = ConfigCrud(session=session, project_id=current_user.project_.id) config = config_crud.exists_or_raise(config_id=config_id) return APIResponse.success_response( data=config, @@ -105,7 +105,7 @@ def update_config( """ Update a specific configuration. """ - config_crud = ConfigCrud(session=session, project_id=current_user.project.id) + config_crud = ConfigCrud(session=session, project_id=current_user.project_.id) config = config_crud.update_or_raise( config_id=config_id, config_update=config_update ) @@ -130,7 +130,7 @@ def delete_config( """ Delete a specific configuration. """ - config_crud = ConfigCrud(session=session, project_id=current_user.project.id) + config_crud = ConfigCrud(session=session, project_id=current_user.project_.id) config_crud.delete_or_raise(config_id=config_id) return APIResponse.success_response( diff --git a/backend/app/api/routes/config/version.py b/backend/app/api/routes/config/version.py index 48246dc85..5f3e8626a 100644 --- a/backend/app/api/routes/config/version.py +++ b/backend/app/api/routes/config/version.py @@ -33,7 +33,7 @@ def create_version( The version number is automatically incremented. """ version_crud = ConfigVersionCrud( - session=session, project_id=current_user.project.id, config_id=config_id + session=session, project_id=current_user.project_.id, config_id=config_id ) version = version_crud.create_or_raise(version_create=version_create) @@ -61,7 +61,7 @@ def list_versions( Ordered by version number in descending order. """ version_crud = ConfigVersionCrud( - session=session, project_id=current_user.project.id, config_id=config_id + session=session, project_id=current_user.project_.id, config_id=config_id ) versions = version_crud.read_all( skip=skip, @@ -91,7 +91,7 @@ def get_version( Get a specific version of a config. """ version_crud = ConfigVersionCrud( - session=session, project_id=current_user.project.id, config_id=config_id + session=session, project_id=current_user.project_.id, config_id=config_id ) version = version_crud.exists_or_raise(version_number=version_number) return APIResponse.success_response( @@ -118,7 +118,7 @@ def delete_version( Delete a specific version of a config. """ version_crud = ConfigVersionCrud( - session=session, project_id=current_user.project.id, config_id=config_id + session=session, project_id=current_user.project_.id, config_id=config_id ) version_crud.delete_or_raise(version_number=version_number)