نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user