Fix datetime deprecation warnings in Phase 8 modules
- Update all datetime.utcnow() to datetime.now(UTC) for Python 3.12+ compatibility - Update all datetime.utcfromtimestamp() to datetime.fromtimestamp(..., UTC) - Fix dataclass default_factory to use lambda: datetime.now(UTC) - Update auth, audit, billing, and realtime modules - Add UTC import from datetime module - Update pytest configuration to include pytest-asyncio - All 28 Phase 8 enterprise tests pass with no warnings Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
398
ontology_platform/ont_platform/api/phase8_app.py
Normal file
398
ontology_platform/ont_platform/api/phase8_app.py
Normal file
@@ -0,0 +1,398 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user