Files
site/apps/api/app/modules/sync/tenant.py
T

93 lines
3.5 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from fastapi import Depends, Header, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials
from app.core.dependencies import bearer, get_current_user
from app.core.security import decode_access_token
from app.modules.sync import repository as sync_repo
from app.modules.sync.schemas import HubAuthContext, TenantContext
from app.modules.users.models import User
@dataclass
class HubPrincipal:
farm_hub_id: str
hub_site_id: str
enterprise_id: str
def get_hub_auth(authorization: str | None = Header(default=None)) -> HubPrincipal:
if not authorization or not authorization.startswith("Hub "):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="HUB_UNAUTHORIZED")
token = authorization[4:].strip()
if ":" not in token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="HUB_UNAUTHORIZED")
hub_site_id, api_key = token.split(":", 1)
hub = sync_repo.verify_hub_credential(hub_site_id.strip(), api_key.strip())
if not hub:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="HUB_UNAUTHORIZED")
return HubPrincipal(farm_hub_id=hub.id, hub_site_id=hub.hub_site_id, enterprise_id=hub.enterprise_id)
def get_tenant_context(
enterprise_id: str,
user: User = Depends(get_current_user),
credentials: HTTPAuthorizationCredentials | None = Depends(bearer),
) -> TenantContext:
if user.is_superuser:
return TenantContext(
enterprise_id=enterprise_id,
enterprise_role="admin",
user_id=user.id,
farm_hub_ids=None,
)
if credentials:
try:
payload = decode_access_token(credentials.credentials)
if payload.get("enterprise_id") == enterprise_id:
farm_ids = payload.get("farm_ids")
return TenantContext(
enterprise_id=enterprise_id,
enterprise_role=payload.get("enterprise_role", "viewer"), # type: ignore[arg-type]
user_id=user.id,
farm_hub_ids=farm_ids,
)
except Exception:
pass
member = sync_repo.get_member(user.id, enterprise_id)
if not member:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
farm_ids = None if member.role == "admin" else sync_repo.list_farm_access(user.id, enterprise_id)
return TenantContext(
enterprise_id=enterprise_id,
enterprise_role=member.role, # type: ignore[arg-type]
user_id=user.id,
farm_hub_ids=farm_ids,
)
def require_farm_access(
farm_hub_id: str,
enterprise_id: str,
tenant: TenantContext,
) -> None:
if tenant.enterprise_role == "admin" or tenant.farm_hub_ids is None:
return
if farm_hub_id not in (tenant.farm_hub_ids or []):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="FARM_ACCESS_FORBIDDEN")
def require_enterprise_admin(tenant: TenantContext = Depends(get_tenant_context)) -> TenantContext:
if tenant.enterprise_role != "admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_ADMIN_ONLY")
return tenant
def require_enterprise_zootech(tenant: TenantContext = Depends(get_tenant_context)) -> TenantContext:
if tenant.enterprise_role not in ("admin", "zootech"):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_ZOOTECH_ONLY")
return tenant