"""FastAPI dependencies: authentication, tenant resolution and capability checks. The single most important rule of this file: **every** organization-scoped endpoint resolves its tenant through ``get_org_context``, which refuses to return anything unless the caller has an active membership. There is no "trust the org_id in the path" path. """ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from fastapi import Depends, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_session from app.core.errors import AuthenticationError, NotFoundError, PermissionDeniedError from app.core.security import decode_access_token from app.domain.identity.models import Membership, Organization, Role, User from app.infrastructure.repositories.identity import ( MembershipRepository, OrganizationRepository, UserRepository, ) from app.interfaces.http.request_context import note_actor if TYPE_CHECKING: from app.application.services.billing_service import BillingService from app.application.services.knowledge_service import KnowledgeService from app.application.services.project_service import ProjectService from app.application.services.sandbox_service import SandboxService from app.application.services.workforce_service import WorkforceService _bearer = HTTPBearer(auto_error=False) @dataclass(slots=True) class OrgContext: """Resolved tenant context for an organization-scoped request.""" organization: Organization membership: Membership user: User @property def org_id(self) -> str: return self.organization.id def require(self, capability: str) -> None: if not self.membership.has(capability): raise PermissionDeniedError("شما دسترسی لازم برای این عملیات را ندارید.") async def get_current_user( request: Request, credentials: HTTPAuthorizationCredentials | None = Depends(_bearer), session: AsyncSession = Depends(get_session), ) -> User: if credentials is None or not credentials.credentials: raise AuthenticationError("برای این عملیات باید وارد شوید.") payload = decode_access_token(credentials.credentials) user_id = payload.get("sub") if not user_id: raise AuthenticationError("توکن نامعتبر است.") user = await UserRepository(session).get(str(user_id)) if user is None or not user.is_active: raise AuthenticationError("حساب کاربری یافت نشد یا غیرفعال است.") # Record the verified actor so any later failure is attributable. Uses the # id from the database, not the one in the token. note_actor(request, user_id=user.id) return user async def get_optional_user( request: Request, credentials: HTTPAuthorizationCredentials | None = Depends(_bearer), session: AsyncSession = Depends(get_session), ) -> User | None: if credentials is None or not credentials.credentials: return None try: return await get_current_user(request, credentials, session) except AuthenticationError: return None async def require_platform_owner(user: User = Depends(get_current_user)) -> User: if not user.is_platform_owner: raise PermissionDeniedError("این بخش فقط برای مالک سامانه قابل دسترسی است.") return user async def get_org_context( request: Request, org_id: str, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ) -> OrgContext: """Resolve ``org_id`` into an authorized tenant context. Returns 404 (not 403) when the caller has no membership, so the API does not leak the existence of other organizations. """ organization = await OrganizationRepository(session).get(org_id) if organization is None: raise NotFoundError("سازمان یافت نشد.") membership = await MembershipRepository(session).get(org_id, user.id) if membership is None or not membership.is_active: raise NotFoundError("سازمان یافت نشد.") # The tenant is only known *after* the membership check passes, so this is # the earliest point at which it is safe to attribute a failure to an org. note_actor(request, organization_id=organization.id, user_id=user.id) return OrgContext(organization=organization, membership=membership, user=user) def require_capability(capability: str): """Dependency factory: ``Depends(require_capability(CAN_MANAGE_PROJECTS))``.""" async def _checker(context: OrgContext = Depends(get_org_context)) -> OrgContext: context.require(capability) return context return _checker def require_role_at_least(minimum: Role): from app.domain.identity.models import ROLE_RANK async def _checker(context: OrgContext = Depends(get_org_context)) -> OrgContext: if ROLE_RANK[context.membership.role] < ROLE_RANK[minimum]: raise PermissionDeniedError("سطح دسترسی شما برای این عملیات کافی نیست.") return context return _checker def client_meta(request: Request) -> tuple[str, str]: """``(ip, user_agent)`` for the audit trail.""" forwarded = request.headers.get("x-forwarded-for", "") ip = ( forwarded.split(",")[0].strip() if forwarded else (request.client.host if request.client else "") ) return ip, request.headers.get("user-agent", "")[:255] # The builders themselves live in ``app.application.container`` so that the # standalone workers compose services exactly the way the API does. These # functions are thin FastAPI dependency wrappers around them. async def get_project_service( session: AsyncSession = Depends(get_session), ) -> ProjectService: """Build the project/domain service with the configured adapters.""" from app.application.container import build_project_service return build_project_service(session) async def get_workforce_service( session: AsyncSession = Depends(get_session), ) -> WorkforceService: """Build the workforce/chat/run service with the configured adapters.""" from app.application.container import build_workforce_service return build_workforce_service(session) async def get_billing_service( session: AsyncSession = Depends(get_session), ) -> BillingService: """Build the billing service with the configured payment adapter.""" from app.application.container import build_billing_service return build_billing_service(session) async def get_knowledge_service( session: AsyncSession = Depends(get_session), ) -> KnowledgeService: """Build the knowledge service with the configured vector adapter.""" from app.application.container import build_knowledge_service return build_knowledge_service(session) async def get_sandbox_service( session: AsyncSession = Depends(get_session), ) -> SandboxService: """Build the sandbox gateway with the configured execution adapter.""" from app.application.container import build_sandbox_service return build_sandbox_service(session)