aiteamutils 0.2.51__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,7 +1,6 @@
1
1
  """보안 관련 유틸리티."""
2
2
  from datetime import datetime, timedelta, UTC
3
- from typing import Dict, Any, Optional, Literal, Callable, Type
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
@@ -9,7 +8,6 @@ from passlib.context import CryptContext
9
8
  import logging
10
9
 
11
10
  from .exceptions import CustomException, ErrorCode
12
- from .database import DatabaseService
13
11
  from .enums import ActivityType
14
12
  from .config import get_settings
15
13
 
@@ -20,151 +18,15 @@ _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 초과 예외를 초기화합니다.
33
-
34
- Args:
35
- detail: 상세 메시지
36
- source_function: 예외가 발생한 함수명
37
- remaining_seconds: 다음 요청까지 남은 시간 (초)
38
- max_requests: 허용된 최대 요청 수
39
- window_seconds: 시간 윈도우 (초)
40
- """
21
+ def __init__(self, detail: str, source_function: str):
41
22
  super().__init__(
42
23
  ErrorCode.RATE_LIMIT_EXCEEDED,
43
24
  detail=detail,
44
- source_function=source_function,
45
- metadata={
46
- "remaining_seconds": remaining_seconds,
47
- "max_requests": max_requests,
48
- "window_seconds": window_seconds
49
- }
50
- )
51
- self.remaining_seconds = remaining_seconds
52
- self.max_requests = max_requests
53
- self.window_seconds = window_seconds
54
-
55
- class SecurityError(CustomException):
56
- """보안 관련 기본 예외."""
57
-
58
- def __init__(
59
- self,
60
- error_code: ErrorCode,
61
- detail: str,
62
- source_function: str,
63
- original_error: Optional[Exception] = None
64
- ):
65
- """보안 관련 예외를 초기화합니다.
66
-
67
- Args:
68
- error_code: 에러 코드
69
- detail: 상세 메시지
70
- source_function: 예외가 발생한 함수명
71
- original_error: 원본 예외
72
- """
73
- super().__init__(
74
- error_code,
75
- detail=detail,
76
- source_function=f"security.{source_function}",
77
- original_error=original_error
78
- )
79
-
80
- class TokenError(SecurityError):
81
- """토큰 관련 예외."""
82
-
83
- def __init__(
84
- self,
85
- detail: str,
86
- source_function: str,
87
- original_error: Optional[Exception] = None
88
- ):
89
- """토큰 관련 예외를 초기화합니다."""
90
- super().__init__(
91
- ErrorCode.INVALID_TOKEN,
92
- detail=detail,
93
- source_function=source_function,
94
- original_error=original_error
95
- )
96
-
97
- class RateLimiter:
98
- """Rate limit 관리 클래스."""
99
-
100
- def __init__(self, max_requests: int, window_seconds: int):
101
- """Rate limiter를 초기화합니다.
102
-
103
- Args:
104
- max_requests: 허용된 최대 요청 수
105
- window_seconds: 시간 윈도우 (초)
106
- """
107
- self.max_requests = max_requests
108
- self.window_seconds = window_seconds
109
- self._cache: Dict[str, Dict[str, Any]] = {}
110
-
111
- def is_allowed(self, key: str) -> bool:
112
- """현재 요청이 허용되는지 확인합니다.
113
-
114
- Args:
115
- key: Rate limit 키
116
-
117
- Returns:
118
- bool: 요청 허용 여부
119
- """
120
- now = datetime.now(UTC)
121
- rate_info = self._cache.get(key)
122
-
123
- if rate_info is None or (now - rate_info["start_time"]).total_seconds() >= self.window_seconds:
124
- self._cache[key] = {
125
- "count": 1,
126
- "start_time": now
127
- }
128
- return True
129
-
130
- if rate_info["count"] >= self.max_requests:
131
- return False
132
-
133
- rate_info["count"] += 1
134
- return True
135
-
136
- def get_remaining_time(self, key: str) -> float:
137
- """남은 시간을 반환합니다.
138
-
139
- Args:
140
- key: Rate limit 키
141
-
142
- Returns:
143
- float: 남은 시간 (초)
144
- """
145
- rate_info = self._cache.get(key)
146
- if not rate_info:
147
- return 0
148
-
149
- now = datetime.now(UTC)
150
- return max(
151
- 0,
152
- self.window_seconds - (now - rate_info["start_time"]).total_seconds()
25
+ source_function=source_function
153
26
  )
154
27
 
155
28
  def verify_password(plain_password: str, hashed_password: str) -> bool:
156
- """비밀번호를 검증합니다.
157
-
158
- Args:
159
- plain_password: 평문 비밀번호
160
- hashed_password: 해시된 비밀번호
161
-
162
- Returns:
163
- bool: 비밀번호 일치 여부
164
-
165
- Raises:
166
- CustomException: 비밀번호 검증 실패 시
167
- """
29
+ """비밀번호를 검증합니다."""
168
30
  try:
169
31
  return pwd_context.verify(plain_password, hashed_password)
170
32
  except Exception as e:
@@ -176,17 +38,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
176
38
  )
177
39
 
178
40
  def hash_password(password: str) -> str:
179
- """비밀번호를 해시화합니다.
180
-
181
- Args:
182
- password: 평문 비밀번호
183
-
184
- Returns:
185
- str: 해시된 비밀번호
186
-
187
- Raises:
188
- CustomException: 비밀번호 해시화 실패 시
189
- """
41
+ """비밀번호를 해시화합니다."""
190
42
  try:
191
43
  return pwd_context.hash(password)
192
44
  except Exception as e:
@@ -206,8 +58,6 @@ def rate_limit(
206
58
  def decorator(func: Callable) -> Callable:
207
59
  @wraps(func)
208
60
  async def wrapper(*args, **kwargs):
209
- logging.info(f"[rate_limit] Starting rate limit check for {func.__name__}")
210
-
211
61
  # Request 객체 찾기
212
62
  request = None
213
63
  for arg in args:
@@ -220,7 +70,6 @@ def rate_limit(
220
70
  request = arg
221
71
  break
222
72
  if not request:
223
- logging.error("[rate_limit] Request object not found in args or kwargs")
224
73
  raise CustomException(
225
74
  ErrorCode.INTERNAL_ERROR,
226
75
  detail="Request object not found",
@@ -234,125 +83,73 @@ def rate_limit(
234
83
  client_ip = request.client.host
235
84
  rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
236
85
 
237
- logging.info(f"[rate_limit] Rate limit key: {rate_limit_key}")
238
-
239
- now = datetime.now(UTC)
240
-
241
- # 현재 rate limit 정보 가져오기
242
- rate_info = _rate_limits.get(rate_limit_key)
243
- logging.info(f"[rate_limit] Current rate info: {rate_info}")
244
-
245
- if rate_info is None or (now - rate_info["start_time"]).total_seconds() >= window_seconds:
246
- # 새로운 rate limit 설정
247
- _rate_limits[rate_limit_key] = {
248
- "count": 1,
249
- "start_time": now
250
- }
251
- logging.info(f"[rate_limit] Created new rate limit: {_rate_limits[rate_limit_key]}")
252
- else:
253
- # 기존 rate limit 업데이트
254
- if rate_info["count"] >= max_requests:
255
- # rate limit 초과
256
- remaining_seconds = window_seconds - (now - rate_info["start_time"]).total_seconds()
257
- logging.warning(f"[rate_limit] Rate limit exceeded. Remaining seconds: {remaining_seconds}")
258
- raise RateLimitExceeded(
259
- detail=rate_limit_key,
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),
260
119
  source_function=func.__name__,
261
- remaining_seconds=remaining_seconds,
262
- max_requests=max_requests,
263
- window_seconds=window_seconds
120
+ original_error=e
264
121
  )
265
- rate_info["count"] += 1
266
- logging.info(f"[rate_limit] Updated rate info: {rate_info}")
267
-
268
- try:
269
- logging.info(f"[rate_limit] Executing original function: {func.__name__}")
270
- result = await func(*args, **kwargs)
271
- logging.info("[rate_limit] Function executed successfully")
272
- return result
122
+
273
123
  except CustomException as e:
274
- logging.error(f"[rate_limit] CustomException occurred: {str(e)}")
275
124
  raise e
276
125
  except Exception as e:
277
- logging.error(f"[rate_limit] Unexpected error occurred: {str(e)}")
278
126
  raise CustomException(
279
127
  ErrorCode.INTERNAL_ERROR,
280
128
  detail=str(e),
281
- source_function=func.__name__,
129
+ source_function="rate_limit",
282
130
  original_error=e
283
131
  )
284
-
132
+
285
133
  return wrapper
286
134
  return decorator
287
135
 
288
- class TokenCreationError(SecurityError):
289
- """토큰 생성 관련 예외."""
290
-
291
- def __init__(
292
- self,
293
- detail: str,
294
- source_function: str,
295
- token_type: str,
296
- original_error: Optional[Exception] = None
297
- ):
298
- """토큰 생성 예외를 초기화합니다.
299
-
300
- Args:
301
- detail: 상세 메시지
302
- source_function: 예외가 발생한 함수명
303
- token_type: 토큰 타입
304
- original_error: 원본 예외
305
- """
306
- super().__init__(
307
- ErrorCode.INTERNAL_ERROR,
308
- detail=detail,
309
- source_function=source_function,
310
- original_error=original_error
311
- )
312
- self.token_type = token_type
313
-
314
136
  async def create_jwt_token(
315
137
  user_data: Dict[str, Any],
316
138
  token_type: Literal["access", "refresh"],
317
- db_service: DatabaseService,
318
- log_model: Type[Base],
139
+ db_service: Any,
319
140
  request: Optional[Request] = None
320
141
  ) -> str:
321
- """JWT 토큰을 생성하고 로그를 기록합니다.
322
-
323
- Args:
324
- user_data: 사용자 데이터 딕셔너리 (username, ulid, name, role_ulid, status, organization 정보 등)
325
- token_type: 토큰 타입 ("access" 또는 "refresh")
326
- db_service: 데이터베이스 서비스
327
- log_model: 로그 모델 클래스
328
- request: FastAPI 요청 객체
329
-
330
- Returns:
331
- str: 생성된 JWT 토큰
332
-
333
- Raises:
334
- CustomException: 토큰 생성 실패 시
335
- """
142
+ """JWT 토큰을 생성하고 로그를 기록합니다."""
336
143
  try:
337
144
  settings = get_settings()
338
145
 
339
- # 필수 필드 검증
340
- required_fields = {"username", "ulid"}
341
- missing_fields = required_fields - set(user_data.keys())
342
- if missing_fields:
343
- raise CustomException(
344
- ErrorCode.REQUIRED_FIELD_MISSING,
345
- detail="|".join(missing_fields),
346
- source_function="security.create_jwt_token"
347
- )
348
-
349
146
  if token_type == "access":
350
- expires_at = datetime.now(UTC) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
147
+ expires_at = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes)
351
148
  token_data = {
352
149
  # 등록 클레임
353
- "iss": settings.TOKEN_ISSUER,
354
- "sub": user_data["ulid"],
355
- "aud": settings.TOKEN_AUDIENCE,
150
+ "iss": settings.token_issuer,
151
+ "sub": user_data["username"],
152
+ "aud": settings.token_audience,
356
153
  "exp": expires_at,
357
154
 
358
155
  # 공개 클레임
@@ -375,18 +172,18 @@ async def create_jwt_token(
375
172
  else: # refresh token
376
173
  expires_at = datetime.now(UTC) + timedelta(days=14)
377
174
  token_data = {
378
- "iss": settings.TOKEN_ISSUER,
379
- "sub": user_data["ulid"],
175
+ "iss": settings.token_issuer,
176
+ "sub": user_data["username"],
380
177
  "exp": expires_at,
381
178
  "token_type": token_type,
382
179
  "user_ulid": user_data["ulid"]
383
180
  }
384
-
181
+
385
182
  try:
386
183
  token = jwt.encode(
387
184
  token_data,
388
- settings.JWT_SECRET,
389
- algorithm=settings.JWT_ALGORITHM
185
+ settings.jwt_secret,
186
+ algorithm=settings.jwt_algorithm
390
187
  )
391
188
  except Exception as e:
392
189
  raise CustomException(
@@ -400,17 +197,16 @@ async def create_jwt_token(
400
197
  try:
401
198
  activity_type = ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED
402
199
  await db_service.create_log(
403
- model=log_model,
404
- log_data={
200
+ {
405
201
  "type": activity_type,
406
202
  "user_ulid": user_data["ulid"],
407
203
  "token": token
408
204
  },
409
- request=request
205
+ request
410
206
  )
411
207
  except Exception as e:
412
208
  # 로그 생성 실패는 토큰 생성에 영향을 주지 않음
413
- pass
209
+ logging.error(f"Failed to create token log: {str(e)}")
414
210
 
415
211
  return token
416
212
 
@@ -428,43 +224,41 @@ async def verify_jwt_token(
428
224
  token: str,
429
225
  expected_type: Optional[Literal["access", "refresh"]] = None
430
226
  ) -> Dict[str, Any]:
431
- """JWT 토큰을 검증합니다.
432
-
433
- Args:
434
- token: 검증할 JWT 토큰
435
- expected_type: 예상되는 토큰 타입
436
-
437
- Returns:
438
- Dict[str, Any]: 토큰 페이로드
439
-
440
- Raises:
441
- CustomException: 토큰 검증 실패 시
442
- """
227
+ """JWT 토큰을 검증합니다."""
443
228
  try:
444
229
  settings = get_settings()
230
+
445
231
  # 토큰 디코딩
446
232
  payload = jwt.decode(
447
233
  token,
448
- settings.JWT_SECRET,
449
- algorithms=[settings.JWT_ALGORITHM],
450
- audience=settings.TOKEN_AUDIENCE,
451
- issuer=settings.TOKEN_ISSUER
234
+ settings.jwt_secret,
235
+ algorithms=[settings.jwt_algorithm],
236
+ audience=settings.token_audience,
237
+ issuer=settings.token_issuer
452
238
  )
453
239
 
454
- # 토큰 타입 검증 (expected_type이 주어진 경우에만)
455
- if expected_type and payload.get("token_type") != expected_type:
240
+ # 토큰 타입 검증
241
+ token_type = payload.get("token_type")
242
+ if not token_type:
456
243
  raise CustomException(
457
244
  ErrorCode.INVALID_TOKEN,
458
- detail=f"token|{expected_type}|{payload.get('token_type')}",
245
+ detail=token,
459
246
  source_function="security.verify_jwt_token"
460
247
  )
461
-
248
+
249
+ if expected_type and token_type != expected_type:
250
+ raise CustomException(
251
+ ErrorCode.INVALID_TOKEN,
252
+ detail=token,
253
+ source_function="security.verify_jwt_token"
254
+ )
255
+
462
256
  return payload
463
257
 
464
258
  except JWTError as e:
465
259
  raise CustomException(
466
260
  ErrorCode.INVALID_TOKEN,
467
- detail=token[:10] + "...",
261
+ detail=token,
468
262
  source_function="security.verify_jwt_token",
469
263
  original_error=e
470
264
  )
@@ -476,4 +270,22 @@ async def verify_jwt_token(
476
270
  detail=str(e),
477
271
  source_function="security.verify_jwt_token",
478
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
479
291
  )
aiteamutils/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  """버전 정보"""
2
- __version__ = "0.2.51"
2
+ __version__ = "0.2.53"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.51
3
+ Version: 0.2.53
4
4
  Summary: AI Team Utilities
5
5
  Project-URL: Homepage, https://github.com/yourusername/aiteamutils
6
6
  Project-URL: Issues, https://github.com/yourusername/aiteamutils/issues
@@ -0,0 +1,16 @@
1
+ aiteamutils/__init__.py,sha256=IAvWobxODQeMIgttFf3e1IGMO-DktLyUmnHeKqGDZWg,1346
2
+ aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
3
+ aiteamutils/base_repository.py,sha256=vqsundoN0h7FVvgqTBEnnJNMcFpvMK0s_nxBWdIYg-U,7846
4
+ aiteamutils/base_service.py,sha256=s2AcA-6_ogOQKgt2xf_3AG2s6tqBceU4nJoXO1II7S8,24588
5
+ aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
6
+ aiteamutils/config.py,sha256=OM_b7g8sqZ3zY_DSF9ry-zn5wn4dlXdx5OhjfTGr0TE,2876
7
+ aiteamutils/database.py,sha256=uSMNWJge5RuQI7zJBJVX24fAHcSPChl-95_2TwK_GOw,39654
8
+ aiteamutils/dependencies.py,sha256=q-OrEOJh4xEpc7ag6nTyey1pQwK9G0ZEDgXB_iTbaM0,6449
9
+ aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
10
+ aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
11
+ aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
12
+ aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
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,,
@@ -1,16 +0,0 @@
1
- aiteamutils/__init__.py,sha256=IAvWobxODQeMIgttFf3e1IGMO-DktLyUmnHeKqGDZWg,1346
2
- aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
3
- aiteamutils/base_repository.py,sha256=qdwQ7Sj2fUqxpDg6cWM48n_QbwPK_VUlG9zTSem8iCk,18968
4
- aiteamutils/base_service.py,sha256=E4dHGE0DvhmRyFplh46SwKJOSF_nUL7OAsCkf_ZJF_8,24733
5
- aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
6
- aiteamutils/config.py,sha256=OM_b7g8sqZ3zY_DSF9ry-zn5wn4dlXdx5OhjfTGr0TE,2876
7
- aiteamutils/database.py,sha256=Be0hr833X3zMz2gaJYugvE9E7o05uBmXKPhZiGFJQTM,38984
8
- aiteamutils/dependencies.py,sha256=0mNmZiPlA8wTj4cgmjGXs6y9kkIQ5RtLZHdEeHxyblo,4490
9
- aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
10
- aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
11
- aiteamutils/security.py,sha256=KSZf3WJAlW0UXVM1GEHwuGy2x27UHwtMr8aq-GuIXZk,16016
12
- aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
13
- aiteamutils/version.py,sha256=8MKWHJHM8zYlzctvZN4uCH_rdHayPQd-Sxyv9JJ2554,42
14
- aiteamutils-0.2.51.dist-info/METADATA,sha256=u7kI-u4xQZcrWK20EtNRzzzJNJvZ22GRD3eI8I7-u-M,1718
15
- aiteamutils-0.2.51.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- aiteamutils-0.2.51.dist-info/RECORD,,