182 lines
5.0 KiB
Python
182 lines
5.0 KiB
Python
|
|
"""감사 로그 모델.
|
||
|
|
|
||
|
|
Phase 8: 감시 및 규정 준수
|
||
|
|
"""
|
||
|
|
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from datetime import datetime, UTC
|
||
|
|
from enum import Enum
|
||
|
|
from typing import Optional, Any, Dict
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
|
||
|
|
class AuditAction(str, Enum):
|
||
|
|
"""감시 작업 타입."""
|
||
|
|
|
||
|
|
# CRUD 작업
|
||
|
|
CREATE = "CREATE"
|
||
|
|
READ = "READ"
|
||
|
|
UPDATE = "UPDATE"
|
||
|
|
DELETE = "DELETE"
|
||
|
|
|
||
|
|
# 분석 작업
|
||
|
|
ANALYZE = "ANALYZE"
|
||
|
|
QUERY = "QUERY"
|
||
|
|
|
||
|
|
# LLM 작업
|
||
|
|
LLM_CALL = "LLM_CALL"
|
||
|
|
LLM_STREAM = "LLM_STREAM"
|
||
|
|
|
||
|
|
# 사용자 관리
|
||
|
|
USER_LOGIN = "USER_LOGIN"
|
||
|
|
USER_LOGOUT = "USER_LOGOUT"
|
||
|
|
USER_CREATED = "USER_CREATED"
|
||
|
|
USER_UPDATED = "USER_UPDATED"
|
||
|
|
USER_DELETED = "USER_DELETED"
|
||
|
|
|
||
|
|
# API 키
|
||
|
|
API_KEY_CREATED = "API_KEY_CREATED"
|
||
|
|
API_KEY_DELETED = "API_KEY_DELETED"
|
||
|
|
API_KEY_USED = "API_KEY_USED"
|
||
|
|
|
||
|
|
# 조직
|
||
|
|
ORG_CREATED = "ORG_CREATED"
|
||
|
|
ORG_UPDATED = "ORG_UPDATED"
|
||
|
|
|
||
|
|
|
||
|
|
class ResourceType(str, Enum):
|
||
|
|
"""리소스 타입."""
|
||
|
|
|
||
|
|
ENTITY = "ENTITY"
|
||
|
|
RELATION = "RELATION"
|
||
|
|
GRAPH = "GRAPH"
|
||
|
|
USER = "USER"
|
||
|
|
API_KEY = "API_KEY"
|
||
|
|
ORGANIZATION = "ORGANIZATION"
|
||
|
|
QUERY = "QUERY"
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Change:
|
||
|
|
"""변경 사항."""
|
||
|
|
|
||
|
|
field_name: str
|
||
|
|
old_value: Any = None
|
||
|
|
new_value: Any = None
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
"""딕셔너리로 변환."""
|
||
|
|
return {
|
||
|
|
"field": self.field_name,
|
||
|
|
"old": str(self.old_value),
|
||
|
|
"new": str(self.new_value),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class AuditLog:
|
||
|
|
"""감시 로그."""
|
||
|
|
|
||
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||
|
|
org_id: str = "" # 조직 ID
|
||
|
|
user_id: str = "" # 사용자 ID
|
||
|
|
action: AuditAction = AuditAction.READ
|
||
|
|
resource_type: ResourceType = ResourceType.ENTITY
|
||
|
|
resource_id: str = "" # 대상 엔티티 ID
|
||
|
|
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||
|
|
ip_address: Optional[str] = None
|
||
|
|
user_agent: Optional[str] = None
|
||
|
|
status: str = "success" # "success", "failed"
|
||
|
|
error_message: Optional[str] = None
|
||
|
|
changes: list = field(default_factory=list) # Change 객체 리스트
|
||
|
|
metadata: dict = field(default_factory=dict)
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
"""딕셔너리로 변환."""
|
||
|
|
return {
|
||
|
|
"id": self.id,
|
||
|
|
"org_id": self.org_id,
|
||
|
|
"user_id": self.user_id,
|
||
|
|
"action": self.action.value,
|
||
|
|
"resource_type": self.resource_type.value,
|
||
|
|
"resource_id": self.resource_id,
|
||
|
|
"timestamp": self.timestamp.isoformat(),
|
||
|
|
"ip_address": self.ip_address,
|
||
|
|
"status": self.status,
|
||
|
|
"error_message": self.error_message,
|
||
|
|
"changes": [c.to_dict() if isinstance(c, Change) else c for c in self.changes],
|
||
|
|
"metadata": self.metadata,
|
||
|
|
}
|
||
|
|
|
||
|
|
def to_neo4j_dict(self) -> dict:
|
||
|
|
"""Neo4j 저장용 딕셔너리."""
|
||
|
|
return {
|
||
|
|
"audit_id": self.id,
|
||
|
|
"org_id": self.org_id,
|
||
|
|
"user_id": self.user_id,
|
||
|
|
"action": self.action.value,
|
||
|
|
"resource_type": self.resource_type.value,
|
||
|
|
"resource_id": self.resource_id,
|
||
|
|
"timestamp": self.timestamp.timestamp(),
|
||
|
|
"ip_address": self.ip_address or "",
|
||
|
|
"status": self.status,
|
||
|
|
"error_message": self.error_message or "",
|
||
|
|
"changes": str(self.changes),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class AuditQuery:
|
||
|
|
"""감사 로그 쿼리."""
|
||
|
|
|
||
|
|
org_id: str = ""
|
||
|
|
user_id: Optional[str] = None
|
||
|
|
action: Optional[AuditAction] = None
|
||
|
|
resource_type: Optional[ResourceType] = None
|
||
|
|
resource_id: Optional[str] = None
|
||
|
|
start_time: Optional[datetime] = None
|
||
|
|
end_time: Optional[datetime] = None
|
||
|
|
status: Optional[str] = None
|
||
|
|
limit: int = 100
|
||
|
|
offset: int = 0
|
||
|
|
|
||
|
|
def to_cypher_filters(self) -> tuple[str, dict]:
|
||
|
|
"""Cypher 필터 생성."""
|
||
|
|
filters = []
|
||
|
|
params = {}
|
||
|
|
|
||
|
|
if self.org_id:
|
||
|
|
filters.append("log.org_id = $org_id")
|
||
|
|
params["org_id"] = self.org_id
|
||
|
|
|
||
|
|
if self.user_id:
|
||
|
|
filters.append("log.user_id = $user_id")
|
||
|
|
params["user_id"] = self.user_id
|
||
|
|
|
||
|
|
if self.action:
|
||
|
|
filters.append("log.action = $action")
|
||
|
|
params["action"] = self.action.value
|
||
|
|
|
||
|
|
if self.resource_type:
|
||
|
|
filters.append("log.resource_type = $resource_type")
|
||
|
|
params["resource_type"] = self.resource_type.value
|
||
|
|
|
||
|
|
if self.resource_id:
|
||
|
|
filters.append("log.resource_id = $resource_id")
|
||
|
|
params["resource_id"] = self.resource_id
|
||
|
|
|
||
|
|
if self.start_time:
|
||
|
|
filters.append("log.timestamp >= $start_time")
|
||
|
|
params["start_time"] = self.start_time.timestamp()
|
||
|
|
|
||
|
|
if self.end_time:
|
||
|
|
filters.append("log.timestamp <= $end_time")
|
||
|
|
params["end_time"] = self.end_time.timestamp()
|
||
|
|
|
||
|
|
if self.status:
|
||
|
|
filters.append("log.status = $status")
|
||
|
|
params["status"] = self.status
|
||
|
|
|
||
|
|
where_clause = " AND ".join(filters) if filters else "1=1"
|
||
|
|
return where_clause, params
|