399 lines
11 KiB
Python
399 lines
11 KiB
Python
|
|
"""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)
|