"""Phase 8 FastAPI 애플리케이션: 멀티테넌트 엔터프라이즈 기능. 기능: - 멀티테넌트 지원 (조직 격리) - WebSocket 실시간 업데이트 - 감시 로그 및 규정 준수 - 비용 관리 및 할당량 - 역할 기반 액세스 제어 """ import logging from typing import Optional, Dict, Any from fastapi import ( FastAPI, APIRouter, WebSocket, WebSocketDisconnect, HTTPException, Depends, Query, Header, ) from fastapi.responses import JSONResponse from ont_platform.auth.models import Organization, CurrentUser from ont_platform.auth.auth import ( JWTAuth, APIKeyAuth, AuthService, get_current_user, ) from ont_platform.auth.rbac import RBAC, Permission, require_permission from ont_platform.audit.logger import AuditLogger from ont_platform.audit.models import AuditAction, ResourceType from ont_platform.billing.calculator import CostCalculator from ont_platform.billing.models import OperationType from ont_platform.realtime.websocket import ConnectionManager from ont_platform.realtime.broadcaster import EventBroadcaster logger = logging.getLogger(__name__) # FastAPI 앱 app = FastAPI( title="Ontology Platform - Phase 8 Enterprise", description="멀티테넌트 엔터프라이즈 기능 지원", version="0.8.0", ) # 라우터 auth_router = APIRouter(prefix="/auth", tags=["auth"]) org_router = APIRouter(prefix="/org", tags=["organization"]) users_router = APIRouter(prefix="/users", tags=["users"]) audit_router = APIRouter(prefix="/audit", tags=["audit"]) billing_router = APIRouter(prefix="/billing", tags=["billing"]) ws_router = APIRouter(tags=["websocket"]) # 전역 인스턴스 connection_manager = ConnectionManager() broadcaster = EventBroadcaster(connection_manager) audit_logger = AuditLogger() cost_calculator = CostCalculator() rbac = RBAC() # 조직 저장소 (테스트용 메모리) organizations: Dict[str, Organization] = {} # ============================================================================ # 인증 엔드포인트 # ============================================================================ @auth_router.post("/login") async def login( email: str = Query(...), password: str = Query(...), org_id: str = Query(...), ) -> Dict[str, Any]: """사용자 로그인.""" try: user, token = await AuthService.login(org_id, email, password) # 감시 로그 await audit_logger.log_action( org_id=org_id, user_id=user.id, action=AuditAction.USER_LOGIN, resource_type=ResourceType.USER, resource_id=user.id, status="success", ) # 비용 기록 await cost_calculator.record_usage( org_id=org_id, user_id=user.id, operation_type=OperationType.API_CALL, quantity=1, ) return { "status": "success", "token": token, "user": user.to_dict(), } except Exception as e: logger.error(f"Login failed: {e}") raise HTTPException(status_code=401, detail="Invalid credentials") @auth_router.post("/register-org") async def register_organization( name: str = Query(...), ) -> Dict[str, Any]: """새 조직 등록.""" org = Organization(name=name) organizations[org.id] = org logger.info(f"Organization registered: {org.id}") return { "status": "success", "org_id": org.id, "name": org.name, "subscription_tier": org.subscription_tier, } @auth_router.post("/api-key") async def create_api_key( name: str = Query(...), current_user: CurrentUser = Depends(get_current_user), ) -> Dict[str, Any]: """API 키 생성.""" # 권한 확인 rbac.check_permission(current_user.role, Permission.MANAGE_API_KEYS.value) # API 키 생성 api_key_record = await AuthService.create_api_key( org_id=current_user.org_id, user_id=current_user.user_id, name=name, ) # 감시 로그 await audit_logger.log_action( org_id=current_user.org_id, user_id=current_user.user_id, action=AuditAction.API_KEY_CREATED, resource_type=ResourceType.API_KEY, resource_id=api_key_record.id, ) return { "status": "success", "api_key_id": api_key_record.id, "name": api_key_record.name, "created_at": api_key_record.created_at.isoformat(), } # ============================================================================ # 조직 엔드포인트 # ============================================================================ @org_router.get("/info") async def get_organization_info( current_user: CurrentUser = Depends(get_current_user), ) -> Dict[str, Any]: """조직 정보 조회.""" org = organizations.get(current_user.org_id) if not org: raise HTTPException(status_code=404, detail="Organization not found") return { "org_id": org.id, "name": org.name, "subscription_tier": org.subscription_tier, "created_at": org.created_at.isoformat(), "is_active": org.is_active, } # ============================================================================ # 감시 로그 엔드포인트 # ============================================================================ @audit_router.get("/logs") async def get_audit_logs( limit: int = Query(100, le=1000), offset: int = Query(0), current_user: CurrentUser = Depends(get_current_user), ) -> Dict[str, Any]: """감시 로그 조회.""" # 권한 확인 rbac.check_permission(current_user.role, Permission.VIEW_AUDIT_LOG.value) logs, total = await audit_logger.query_logs( limit=limit, offset=offset, ) return { "status": "success", "total": total, "logs": [log.to_dict() for log in logs], } @audit_router.get("/audit-trail/{resource_id}") async def get_audit_trail( resource_id: str, limit: int = Query(100, le=1000), current_user: CurrentUser = Depends(get_current_user), ) -> Dict[str, Any]: """리소스 감시 이력 조회.""" # 권한 확인 rbac.check_permission(current_user.role, Permission.VIEW_AUDIT_LOG.value) logs = await audit_logger.get_audit_trail( org_id=current_user.org_id, resource_id=resource_id, limit=limit, ) return { "status": "success", "resource_id": resource_id, "total": len(logs), "logs": [log.to_dict() for log in logs], } @audit_router.get("/statistics") async def get_audit_statistics( days: int = Query(30, ge=1, le=365), current_user: CurrentUser = Depends(get_current_user), ) -> Dict[str, Any]: """감시 통계 조회.""" # 권한 확인 rbac.check_permission(current_user.role, Permission.VIEW_AUDIT_LOG.value) stats = await audit_logger.get_statistics( org_id=current_user.org_id, days=days, ) return { "status": "success", "statistics": stats, } # ============================================================================ # 비용 관리 엔드포인트 # ============================================================================ @billing_router.get("/usage") async def get_usage_statistics( days: int = Query(30, ge=1, le=365), current_user: CurrentUser = Depends(get_current_user), ) -> Dict[str, Any]: """사용량 통계 조회.""" stats = await cost_calculator.get_usage_statistics( org_id=current_user.org_id, period_days=days, ) return { "status": "success", "statistics": stats.to_dict(), } @billing_router.get("/forecast") async def get_cost_forecast( current_user: CurrentUser = Depends(get_current_user), ) -> Dict[str, Any]: """비용 예측 조회.""" forecast = await cost_calculator.get_cost_forecast( org_id=current_user.org_id, ) return { "status": "success", "forecast": forecast, } # ============================================================================ # WebSocket 엔드포인트 # ============================================================================ @ws_router.websocket("/ws/{org_id}") async def websocket_endpoint( org_id: str, websocket: WebSocket, token: Optional[str] = None, ): """WebSocket 실시간 업데이트. Usage: ws://localhost:8000/ws/{org_id}?token={jwt_token} """ # 토큰 검증 if token: try: payload = JWTAuth.verify_token(token) if payload.org_id != org_id: await websocket.close(code=4003, reason="Org mismatch") return except Exception as e: logger.warning(f"WebSocket auth failed: {e}") await websocket.close(code=4001, reason="Unauthorized") return await connection_manager.connect(org_id, websocket) try: # 연결 유지 while True: data = await websocket.receive_text() logger.debug(f"WebSocket message from {org_id}: {data}") # 간단한 ping/pong if data == "ping": await websocket.send_json({"type": "pong"}) except WebSocketDisconnect: await connection_manager.disconnect(websocket) logger.info(f"WebSocket disconnected: {org_id}") except Exception as e: logger.error(f"WebSocket error: {e}") await connection_manager.disconnect(websocket) # ============================================================================ # 헬스 체크 # ============================================================================ @app.get("/health") async def health_check() -> Dict[str, Any]: """헬스 체크.""" return { "status": "healthy", "version": "0.8.0", "phase": "8 (Enterprise)", "components": { "auth": "ok", "audit": "ok", "billing": "ok", "websocket": f"{connection_manager.get_connection_count()} connections", }, } @app.get("/info") async def get_platform_info() -> Dict[str, Any]: """플랫폼 정보.""" return { "platform": "Ontology System Construction Platform", "phase": "8 (Enterprise)", "version": "0.8.0", "features": { "multitenant": True, "websocket": True, "audit_logging": True, "billing": True, "rbac": True, }, "organizations": len(organizations), "active_websocket_connections": connection_manager.get_connection_count(), } # ============================================================================ # 라우터 등록 # ============================================================================ app.include_router(auth_router) app.include_router(org_router) app.include_router(users_router) app.include_router(audit_router) app.include_router(billing_router) app.include_router(ws_router) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8002)