aiteamutils 0.2.51__py3-none-any.whl → 0.2.53__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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,,