aiteamutils 0.2.52__py3-none-any.whl → 0.2.53__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aiteamutils/security.py
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
"""보안 관련 유틸리티."""
|
2
2
|
from datetime import datetime, timedelta, UTC
|
3
|
-
from typing import Dict, Any, Optional, Literal, Callable,
|
4
|
-
from sqlalchemy.orm import DeclarativeBase as Base
|
3
|
+
from typing import Dict, Any, Optional, Literal, Callable, TYPE_CHECKING
|
5
4
|
from fastapi import Request, HTTPException, status
|
6
5
|
from functools import wraps
|
7
6
|
from jose import jwt, JWTError
|
8
7
|
from passlib.context import CryptContext
|
9
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
10
8
|
import logging
|
11
9
|
|
12
10
|
from .exceptions import CustomException, ErrorCode
|
@@ -20,29 +18,12 @@ _rate_limits: Dict[str, Dict[str, Any]] = {}
|
|
20
18
|
|
21
19
|
class RateLimitExceeded(CustomException):
|
22
20
|
"""Rate limit 초과 예외."""
|
23
|
-
|
24
|
-
def __init__(
|
25
|
-
self,
|
26
|
-
detail: str,
|
27
|
-
source_function: str,
|
28
|
-
remaining_seconds: float,
|
29
|
-
max_requests: int,
|
30
|
-
window_seconds: int
|
31
|
-
):
|
32
|
-
"""Rate limit 초과 예외를 초기화합니다."""
|
21
|
+
def __init__(self, detail: str, source_function: str):
|
33
22
|
super().__init__(
|
34
23
|
ErrorCode.RATE_LIMIT_EXCEEDED,
|
35
24
|
detail=detail,
|
36
|
-
source_function=source_function
|
37
|
-
metadata={
|
38
|
-
"remaining_seconds": remaining_seconds,
|
39
|
-
"max_requests": max_requests,
|
40
|
-
"window_seconds": window_seconds
|
41
|
-
}
|
25
|
+
source_function=source_function
|
42
26
|
)
|
43
|
-
self.remaining_seconds = remaining_seconds
|
44
|
-
self.max_requests = max_requests
|
45
|
-
self.window_seconds = window_seconds
|
46
27
|
|
47
28
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
48
29
|
"""비밀번호를 검증합니다."""
|
@@ -68,28 +49,97 @@ def hash_password(password: str) -> str:
|
|
68
49
|
original_error=e
|
69
50
|
)
|
70
51
|
|
52
|
+
def rate_limit(
|
53
|
+
max_requests: int,
|
54
|
+
window_seconds: int,
|
55
|
+
key_func: Optional[Callable] = None
|
56
|
+
):
|
57
|
+
"""Rate limiting 데코레이터."""
|
58
|
+
def decorator(func: Callable) -> Callable:
|
59
|
+
@wraps(func)
|
60
|
+
async def wrapper(*args, **kwargs):
|
61
|
+
# Request 객체 찾기
|
62
|
+
request = None
|
63
|
+
for arg in args:
|
64
|
+
if isinstance(arg, Request):
|
65
|
+
request = arg
|
66
|
+
break
|
67
|
+
if not request:
|
68
|
+
for arg in kwargs.values():
|
69
|
+
if isinstance(arg, Request):
|
70
|
+
request = arg
|
71
|
+
break
|
72
|
+
if not request:
|
73
|
+
raise CustomException(
|
74
|
+
ErrorCode.INTERNAL_ERROR,
|
75
|
+
detail="Request object not found",
|
76
|
+
source_function="rate_limit"
|
77
|
+
)
|
78
|
+
|
79
|
+
# 레이트 리밋 키 생성
|
80
|
+
if key_func:
|
81
|
+
rate_limit_key = f"rate_limit:{key_func(request)}"
|
82
|
+
else:
|
83
|
+
client_ip = request.client.host
|
84
|
+
rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
|
85
|
+
|
86
|
+
try:
|
87
|
+
now = datetime.now(UTC)
|
88
|
+
|
89
|
+
# 현재 rate limit 정보 가져오기
|
90
|
+
rate_info = _rate_limits.get(rate_limit_key)
|
91
|
+
|
92
|
+
if rate_info is None or (now - rate_info["start_time"]).total_seconds() >= window_seconds:
|
93
|
+
# 새로운 rate limit 설정
|
94
|
+
_rate_limits[rate_limit_key] = {
|
95
|
+
"count": 1,
|
96
|
+
"start_time": now
|
97
|
+
}
|
98
|
+
else:
|
99
|
+
# 기존 rate limit 업데이트
|
100
|
+
if rate_info["count"] >= max_requests:
|
101
|
+
# rate limit 초과
|
102
|
+
remaining_seconds = window_seconds - (now - rate_info["start_time"]).total_seconds()
|
103
|
+
raise CustomException(
|
104
|
+
ErrorCode.RATE_LIMIT_EXCEEDED,
|
105
|
+
detail=f"{int(remaining_seconds)}",
|
106
|
+
source_function=func.__name__
|
107
|
+
)
|
108
|
+
rate_info["count"] += 1
|
109
|
+
|
110
|
+
try:
|
111
|
+
# 원래 함수 실행
|
112
|
+
return await func(*args, **kwargs)
|
113
|
+
except CustomException as e:
|
114
|
+
raise e
|
115
|
+
except Exception as e:
|
116
|
+
raise CustomException(
|
117
|
+
ErrorCode.INTERNAL_ERROR,
|
118
|
+
detail=str(e),
|
119
|
+
source_function=func.__name__,
|
120
|
+
original_error=e
|
121
|
+
)
|
122
|
+
|
123
|
+
except CustomException as e:
|
124
|
+
raise e
|
125
|
+
except Exception as e:
|
126
|
+
raise CustomException(
|
127
|
+
ErrorCode.INTERNAL_ERROR,
|
128
|
+
detail=str(e),
|
129
|
+
source_function="rate_limit",
|
130
|
+
original_error=e
|
131
|
+
)
|
132
|
+
|
133
|
+
return wrapper
|
134
|
+
return decorator
|
135
|
+
|
71
136
|
async def create_jwt_token(
|
72
137
|
user_data: Dict[str, Any],
|
73
138
|
token_type: Literal["access", "refresh"],
|
74
|
-
|
75
|
-
log_model: Type[Base],
|
139
|
+
db_service: Any,
|
76
140
|
request: Optional[Request] = None
|
77
141
|
) -> str:
|
78
|
-
"""JWT 토큰을
|
79
|
-
|
80
|
-
Args:
|
81
|
-
user_data (Dict[str, Any]): 사용자 데이터
|
82
|
-
token_type (Literal["access", "refresh"]): 토큰 타입
|
83
|
-
session (AsyncSession): 데이터베이스 세션
|
84
|
-
log_model (Type[Base]): 로그 모델
|
85
|
-
request (Optional[Request], optional): FastAPI 요청 객체. Defaults to None.
|
86
|
-
|
87
|
-
Returns:
|
88
|
-
str: 생성된 JWT 토큰
|
89
|
-
|
90
|
-
Raises:
|
91
|
-
CustomException: 토큰 생성 실패 시
|
92
|
-
"""
|
142
|
+
"""JWT 토큰을 생성하고 로그를 기록합니다."""
|
93
143
|
try:
|
94
144
|
settings = get_settings()
|
95
145
|
|
@@ -98,7 +148,7 @@ async def create_jwt_token(
|
|
98
148
|
token_data = {
|
99
149
|
# 등록 클레임
|
100
150
|
"iss": settings.token_issuer,
|
101
|
-
"sub": user_data["
|
151
|
+
"sub": user_data["username"],
|
102
152
|
"aud": settings.token_audience,
|
103
153
|
"exp": expires_at,
|
104
154
|
|
@@ -123,7 +173,7 @@ async def create_jwt_token(
|
|
123
173
|
expires_at = datetime.now(UTC) + timedelta(days=14)
|
124
174
|
token_data = {
|
125
175
|
"iss": settings.token_issuer,
|
126
|
-
"sub": user_data["
|
176
|
+
"sub": user_data["username"],
|
127
177
|
"exp": expires_at,
|
128
178
|
"token_type": token_type,
|
129
179
|
"user_ulid": user_data["ulid"]
|
@@ -146,13 +196,14 @@ async def create_jwt_token(
|
|
146
196
|
# 로그 생성
|
147
197
|
try:
|
148
198
|
activity_type = ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
199
|
+
await db_service.create_log(
|
200
|
+
{
|
201
|
+
"type": activity_type,
|
202
|
+
"user_ulid": user_data["ulid"],
|
203
|
+
"token": token
|
204
|
+
},
|
205
|
+
request
|
153
206
|
)
|
154
|
-
session.add(log_entry)
|
155
|
-
await session.flush()
|
156
207
|
except Exception as e:
|
157
208
|
# 로그 생성 실패는 토큰 생성에 영향을 주지 않음
|
158
209
|
logging.error(f"Failed to create token log: {str(e)}")
|
@@ -173,18 +224,7 @@ async def verify_jwt_token(
|
|
173
224
|
token: str,
|
174
225
|
expected_type: Optional[Literal["access", "refresh"]] = None
|
175
226
|
) -> Dict[str, Any]:
|
176
|
-
"""JWT 토큰을 검증합니다.
|
177
|
-
|
178
|
-
Args:
|
179
|
-
token: 검증할 JWT 토큰
|
180
|
-
expected_type: 예상되는 토큰 타입
|
181
|
-
|
182
|
-
Returns:
|
183
|
-
Dict[str, Any]: 토큰 페이로드
|
184
|
-
|
185
|
-
Raises:
|
186
|
-
CustomException: 토큰 검증 실패 시
|
187
|
-
"""
|
227
|
+
"""JWT 토큰을 검증합니다."""
|
188
228
|
try:
|
189
229
|
settings = get_settings()
|
190
230
|
|
@@ -202,23 +242,14 @@ async def verify_jwt_token(
|
|
202
242
|
if not token_type:
|
203
243
|
raise CustomException(
|
204
244
|
ErrorCode.INVALID_TOKEN,
|
205
|
-
detail=
|
245
|
+
detail=token,
|
206
246
|
source_function="security.verify_jwt_token"
|
207
247
|
)
|
208
248
|
|
209
249
|
if expected_type and token_type != expected_type:
|
210
250
|
raise CustomException(
|
211
251
|
ErrorCode.INVALID_TOKEN,
|
212
|
-
detail=
|
213
|
-
source_function="security.verify_jwt_token"
|
214
|
-
)
|
215
|
-
|
216
|
-
# 사용자 식별자 검증
|
217
|
-
user_ulid = payload.get("user_ulid")
|
218
|
-
if not user_ulid:
|
219
|
-
raise CustomException(
|
220
|
-
ErrorCode.INVALID_TOKEN,
|
221
|
-
detail="User identifier is missing",
|
252
|
+
detail=token,
|
222
253
|
source_function="security.verify_jwt_token"
|
223
254
|
)
|
224
255
|
|
@@ -227,7 +258,7 @@ async def verify_jwt_token(
|
|
227
258
|
except JWTError as e:
|
228
259
|
raise CustomException(
|
229
260
|
ErrorCode.INVALID_TOKEN,
|
230
|
-
detail=
|
261
|
+
detail=token,
|
231
262
|
source_function="security.verify_jwt_token",
|
232
263
|
original_error=e
|
233
264
|
)
|
@@ -239,4 +270,22 @@ async def verify_jwt_token(
|
|
239
270
|
detail=str(e),
|
240
271
|
source_function="security.verify_jwt_token",
|
241
272
|
original_error=e
|
273
|
+
)
|
274
|
+
|
275
|
+
def validate_token(token: str) -> Dict[str, Any]:
|
276
|
+
"""JWT 토큰을 검증하고 페이로드를 반환합니다."""
|
277
|
+
try:
|
278
|
+
settings = get_settings()
|
279
|
+
payload = jwt.decode(
|
280
|
+
token,
|
281
|
+
settings.jwt_secret,
|
282
|
+
algorithms=[settings.jwt_algorithm]
|
283
|
+
)
|
284
|
+
return payload
|
285
|
+
except JWTError as e:
|
286
|
+
raise CustomException(
|
287
|
+
ErrorCode.INVALID_TOKEN,
|
288
|
+
detail=token,
|
289
|
+
source_function="security.validate_token",
|
290
|
+
original_error=e
|
242
291
|
)
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.53"
|
@@ -8,9 +8,9 @@ aiteamutils/database.py,sha256=uSMNWJge5RuQI7zJBJVX24fAHcSPChl-95_2TwK_GOw,39654
|
|
8
8
|
aiteamutils/dependencies.py,sha256=q-OrEOJh4xEpc7ag6nTyey1pQwK9G0ZEDgXB_iTbaM0,6449
|
9
9
|
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
10
10
|
aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
|
11
|
-
aiteamutils/security.py,sha256=
|
11
|
+
aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
|
12
12
|
aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
|
13
|
-
aiteamutils/version.py,sha256=
|
14
|
-
aiteamutils-0.2.
|
15
|
-
aiteamutils-0.2.
|
16
|
-
aiteamutils-0.2.
|
13
|
+
aiteamutils/version.py,sha256=l6fF0dCvrM5gxQHard3VFOKO2YQLlkx_RlQ9Azm0-pk,42
|
14
|
+
aiteamutils-0.2.53.dist-info/METADATA,sha256=LKH9v0dzZbm_PRCEZ-NS-ASC1p6pRyU3h8Mk6Nw4IYE,1718
|
15
|
+
aiteamutils-0.2.53.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.53.dist-info/RECORD,,
|
File without changes
|