159 lines
4.4 KiB
Python
159 lines
4.4 KiB
Python
|
|
"""인증 모델 (Organization, User, APIKey).
|
||
|
|
|
||
|
|
Phase 8: 멀티테넌트 지원
|
||
|
|
"""
|
||
|
|
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from datetime import datetime, UTC
|
||
|
|
from typing import Optional, List
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Organization:
|
||
|
|
"""조직 (테넌트)."""
|
||
|
|
|
||
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||
|
|
name: str = ""
|
||
|
|
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||
|
|
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||
|
|
is_active: bool = True
|
||
|
|
subscription_tier: str = "free" # "free", "pro", "enterprise"
|
||
|
|
max_users: int = 5 # Free tier 기본값
|
||
|
|
storage_limit_gb: int = 1
|
||
|
|
api_quota_monthly: int = 10000
|
||
|
|
metadata: dict = field(default_factory=dict)
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
"""딕셔너리로 변환."""
|
||
|
|
return {
|
||
|
|
"id": self.id,
|
||
|
|
"name": self.name,
|
||
|
|
"created_at": self.created_at.isoformat(),
|
||
|
|
"is_active": self.is_active,
|
||
|
|
"subscription_tier": self.subscription_tier,
|
||
|
|
"max_users": self.max_users,
|
||
|
|
"storage_limit_gb": self.storage_limit_gb,
|
||
|
|
"api_quota_monthly": self.api_quota_monthly,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class User:
|
||
|
|
"""사용자."""
|
||
|
|
|
||
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||
|
|
org_id: str = "" # 조직 ID (FK)
|
||
|
|
email: str = ""
|
||
|
|
username: str = ""
|
||
|
|
hashed_password: str = ""
|
||
|
|
role: str = "viewer" # "admin", "editor", "viewer"
|
||
|
|
is_active: bool = True
|
||
|
|
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||
|
|
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||
|
|
last_login: Optional[datetime] = None
|
||
|
|
metadata: dict = field(default_factory=dict)
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
"""딕셔너리로 변환."""
|
||
|
|
return {
|
||
|
|
"id": self.id,
|
||
|
|
"org_id": self.org_id,
|
||
|
|
"email": self.email,
|
||
|
|
"username": self.username,
|
||
|
|
"role": self.role,
|
||
|
|
"is_active": self.is_active,
|
||
|
|
"created_at": self.created_at.isoformat(),
|
||
|
|
"last_login": self.last_login.isoformat() if self.last_login else None,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class APIKey:
|
||
|
|
"""API 키."""
|
||
|
|
|
||
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||
|
|
org_id: str = "" # 조직 ID (FK)
|
||
|
|
key_hash: str = "" # SHA256 해시 (원본은 저장하지 않음)
|
||
|
|
name: str = ""
|
||
|
|
description: str = ""
|
||
|
|
is_active: bool = True
|
||
|
|
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||
|
|
last_used: Optional[datetime] = None
|
||
|
|
last_used_ip: Optional[str] = None
|
||
|
|
metadata: dict = field(default_factory=dict)
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
"""딕셔너리로 변환 (해시만 포함)."""
|
||
|
|
return {
|
||
|
|
"id": self.id,
|
||
|
|
"org_id": self.org_id,
|
||
|
|
"name": self.name,
|
||
|
|
"description": self.description,
|
||
|
|
"is_active": self.is_active,
|
||
|
|
"created_at": self.created_at.isoformat(),
|
||
|
|
"last_used": self.last_used.isoformat() if self.last_used else None,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class TokenPayload:
|
||
|
|
"""JWT 토큰 페이로드."""
|
||
|
|
|
||
|
|
user_id: str
|
||
|
|
org_id: str
|
||
|
|
email: str
|
||
|
|
role: str
|
||
|
|
exp: int # Unix timestamp
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
"""딕셔너리로 변환."""
|
||
|
|
return {
|
||
|
|
"user_id": self.user_id,
|
||
|
|
"org_id": self.org_id,
|
||
|
|
"email": self.email,
|
||
|
|
"role": self.role,
|
||
|
|
"exp": self.exp,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class AuthCredentials:
|
||
|
|
"""인증 자격증명."""
|
||
|
|
|
||
|
|
email: Optional[str] = None
|
||
|
|
password: Optional[str] = None
|
||
|
|
api_key: Optional[str] = None
|
||
|
|
token: Optional[str] = None
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class CurrentUser:
|
||
|
|
"""현재 인증된 사용자."""
|
||
|
|
|
||
|
|
user_id: str
|
||
|
|
org_id: str
|
||
|
|
email: str
|
||
|
|
username: str
|
||
|
|
role: str
|
||
|
|
is_active: bool
|
||
|
|
|
||
|
|
def has_permission(self, action: str) -> bool:
|
||
|
|
"""사용자가 작업 권한을 가지고 있는지 확인."""
|
||
|
|
from ont_platform.auth.rbac import RBAC
|
||
|
|
|
||
|
|
rbac = RBAC()
|
||
|
|
permissions = rbac.get_permissions(self.role)
|
||
|
|
return action in permissions
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
"""딕셔너리로 변환."""
|
||
|
|
return {
|
||
|
|
"user_id": self.user_id,
|
||
|
|
"org_id": self.org_id,
|
||
|
|
"email": self.email,
|
||
|
|
"username": self.username,
|
||
|
|
"role": self.role,
|
||
|
|
"is_active": self.is_active,
|
||
|
|
}
|