Files

399 lines
11 KiB
Python
Raw Permalink Normal View History

"""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)