aiteamutils 0.2.50__py3-none-any.whl → 0.2.52__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
@@ -6,10 +6,10 @@ from fastapi import Request, HTTPException, status
6
6
  from functools import wraps
7
7
  from jose import jwt, JWTError
8
8
  from passlib.context import CryptContext
9
+ from sqlalchemy.ext.asyncio import AsyncSession
9
10
  import logging
10
11
 
11
12
  from .exceptions import CustomException, ErrorCode
12
- from .database import DatabaseService
13
13
  from .enums import ActivityType
14
14
  from .config import get_settings
15
15
 
@@ -29,15 +29,7 @@ class RateLimitExceeded(CustomException):
29
29
  max_requests: int,
30
30
  window_seconds: int
31
31
  ):
32
- """Rate limit 초과 예외를 초기화합니다.
33
-
34
- Args:
35
- detail: 상세 메시지
36
- source_function: 예외가 발생한 함수명
37
- remaining_seconds: 다음 요청까지 남은 시간 (초)
38
- max_requests: 허용된 최대 요청 수
39
- window_seconds: 시간 윈도우 (초)
40
- """
32
+ """Rate limit 초과 예외를 초기화합니다."""
41
33
  super().__init__(
42
34
  ErrorCode.RATE_LIMIT_EXCEEDED,
43
35
  detail=detail,
@@ -52,119 +44,8 @@ class RateLimitExceeded(CustomException):
52
44
  self.max_requests = max_requests
53
45
  self.window_seconds = window_seconds
54
46
 
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()
153
- )
154
-
155
47
  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
- """
48
+ """비밀번호를 검증합니다."""
168
49
  try:
169
50
  return pwd_context.verify(plain_password, hashed_password)
170
51
  except Exception as e:
@@ -176,17 +57,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
176
57
  )
177
58
 
178
59
  def hash_password(password: str) -> str:
179
- """비밀번호를 해시화합니다.
180
-
181
- Args:
182
- password: 평문 비밀번호
183
-
184
- Returns:
185
- str: 해시된 비밀번호
186
-
187
- Raises:
188
- CustomException: 비밀번호 해시화 실패 시
189
- """
60
+ """비밀번호를 해시화합니다."""
190
61
  try:
191
62
  return pwd_context.hash(password)
192
63
  except Exception as e:
@@ -197,135 +68,21 @@ def hash_password(password: str) -> str:
197
68
  original_error=e
198
69
  )
199
70
 
200
- def rate_limit(
201
- max_requests: int,
202
- window_seconds: int,
203
- key_func: Optional[Callable] = None
204
- ):
205
- """Rate limiting 데코레이터."""
206
- def decorator(func: Callable) -> Callable:
207
- @wraps(func)
208
- async def wrapper(*args, **kwargs):
209
- logging.info(f"[rate_limit] Starting rate limit check for {func.__name__}")
210
-
211
- # Request 객체 찾기
212
- request = None
213
- for arg in args:
214
- if isinstance(arg, Request):
215
- request = arg
216
- break
217
- if not request:
218
- for arg in kwargs.values():
219
- if isinstance(arg, Request):
220
- request = arg
221
- break
222
- if not request:
223
- logging.error("[rate_limit] Request object not found in args or kwargs")
224
- raise CustomException(
225
- ErrorCode.INTERNAL_ERROR,
226
- detail="Request object not found",
227
- source_function="rate_limit"
228
- )
229
-
230
- # 레이트 리밋 키 생성
231
- if key_func:
232
- rate_limit_key = f"rate_limit:{key_func(request)}"
233
- else:
234
- client_ip = request.client.host
235
- rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
236
-
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,
260
- source_function=func.__name__,
261
- remaining_seconds=remaining_seconds,
262
- max_requests=max_requests,
263
- window_seconds=window_seconds
264
- )
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
273
- except CustomException as e:
274
- logging.error(f"[rate_limit] CustomException occurred: {str(e)}")
275
- raise e
276
- except Exception as e:
277
- logging.error(f"[rate_limit] Unexpected error occurred: {str(e)}")
278
- raise CustomException(
279
- ErrorCode.INTERNAL_ERROR,
280
- detail=str(e),
281
- source_function=func.__name__,
282
- original_error=e
283
- )
284
-
285
- return wrapper
286
- return decorator
287
-
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
71
  async def create_jwt_token(
315
72
  user_data: Dict[str, Any],
316
73
  token_type: Literal["access", "refresh"],
317
- db_service: DatabaseService,
74
+ session: AsyncSession,
318
75
  log_model: Type[Base],
319
76
  request: Optional[Request] = None
320
77
  ) -> str:
321
- """JWT 토큰을 생성하고 로그를 기록합니다.
78
+ """JWT 토큰을 생성합니다.
322
79
 
323
80
  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 요청 객체
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.
329
86
 
330
87
  Returns:
331
88
  str: 생성된 JWT 토큰
@@ -336,23 +93,13 @@ async def create_jwt_token(
336
93
  try:
337
94
  settings = get_settings()
338
95
 
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
96
  if token_type == "access":
350
- expires_at = datetime.now(UTC) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
97
+ expires_at = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes)
351
98
  token_data = {
352
99
  # 등록 클레임
353
- "iss": settings.TOKEN_ISSUER,
100
+ "iss": settings.token_issuer,
354
101
  "sub": user_data["ulid"],
355
- "aud": settings.TOKEN_AUDIENCE,
102
+ "aud": settings.token_audience,
356
103
  "exp": expires_at,
357
104
 
358
105
  # 공개 클레임
@@ -375,18 +122,18 @@ async def create_jwt_token(
375
122
  else: # refresh token
376
123
  expires_at = datetime.now(UTC) + timedelta(days=14)
377
124
  token_data = {
378
- "iss": settings.TOKEN_ISSUER,
125
+ "iss": settings.token_issuer,
379
126
  "sub": user_data["ulid"],
380
127
  "exp": expires_at,
381
128
  "token_type": token_type,
382
129
  "user_ulid": user_data["ulid"]
383
130
  }
384
-
131
+
385
132
  try:
386
133
  token = jwt.encode(
387
134
  token_data,
388
- settings.JWT_SECRET,
389
- algorithm=settings.JWT_ALGORITHM
135
+ settings.jwt_secret,
136
+ algorithm=settings.jwt_algorithm
390
137
  )
391
138
  except Exception as e:
392
139
  raise CustomException(
@@ -399,18 +146,16 @@ async def create_jwt_token(
399
146
  # 로그 생성
400
147
  try:
401
148
  activity_type = ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED
402
- await db_service.create_log(
403
- model=log_model,
404
- log_data={
405
- "type": activity_type,
406
- "user_ulid": user_data["ulid"],
407
- "token": token
408
- },
409
- request=request
149
+ log_entry = log_model(
150
+ type=activity_type,
151
+ user_ulid=user_data["ulid"],
152
+ token=token
410
153
  )
154
+ session.add(log_entry)
155
+ await session.flush()
411
156
  except Exception as e:
412
157
  # 로그 생성 실패는 토큰 생성에 영향을 주지 않음
413
- pass
158
+ logging.error(f"Failed to create token log: {str(e)}")
414
159
 
415
160
  return token
416
161
 
@@ -442,29 +187,47 @@ async def verify_jwt_token(
442
187
  """
443
188
  try:
444
189
  settings = get_settings()
190
+
445
191
  # 토큰 디코딩
446
192
  payload = jwt.decode(
447
193
  token,
448
- settings.JWT_SECRET,
449
- algorithms=[settings.JWT_ALGORITHM],
450
- audience=settings.TOKEN_AUDIENCE,
451
- issuer=settings.TOKEN_ISSUER
194
+ settings.jwt_secret,
195
+ algorithms=[settings.jwt_algorithm],
196
+ audience=settings.token_audience,
197
+ issuer=settings.token_issuer
452
198
  )
453
199
 
454
- # 토큰 타입 검증 (expected_type이 주어진 경우에만)
455
- if expected_type and payload.get("token_type") != expected_type:
200
+ # 토큰 타입 검증
201
+ token_type = payload.get("token_type")
202
+ if not token_type:
456
203
  raise CustomException(
457
204
  ErrorCode.INVALID_TOKEN,
458
- detail=f"token|{expected_type}|{payload.get('token_type')}",
205
+ detail="Token type is missing",
459
206
  source_function="security.verify_jwt_token"
460
207
  )
461
-
208
+
209
+ if expected_type and token_type != expected_type:
210
+ raise CustomException(
211
+ ErrorCode.INVALID_TOKEN,
212
+ detail=f"Expected {expected_type} token but got {token_type}",
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",
222
+ source_function="security.verify_jwt_token"
223
+ )
224
+
462
225
  return payload
463
226
 
464
227
  except JWTError as e:
465
228
  raise CustomException(
466
229
  ErrorCode.INVALID_TOKEN,
467
- detail=token[:10] + "...",
230
+ detail=str(e),
468
231
  source_function="security.verify_jwt_token",
469
232
  original_error=e
470
233
  )
aiteamutils/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  """버전 정보"""
2
- __version__ = "0.2.50"
2
+ __version__ = "0.2.52"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.50
3
+ Version: 0.2.52
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=AUcGlNZeURF9AIF87p9SvWYKVao2_CISk8B6WasF-mo,8050
12
+ aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
13
+ aiteamutils/version.py,sha256=jkNi_8VOucj0ySHwq10MaZLBXEjXin60tdcDfPSEO5A,42
14
+ aiteamutils-0.2.52.dist-info/METADATA,sha256=1XX034T6pG0yeYvGKWcPiSbUpmGF1NFlP8wQELSqyRo,1718
15
+ aiteamutils-0.2.52.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ aiteamutils-0.2.52.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=rEKuZ8_3eBjP1KpTNVgWrVLAtKX_IkZJzul2pa8bDlg,40075
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=lxXpdmBOudu-YVW902wEuXxWiy_elYifbZv5G4sOhdo,42
14
- aiteamutils-0.2.50.dist-info/METADATA,sha256=WjFxSSNzciuhgenfILvBNXtMKSY-HIAd6m88tZXrEiU,1718
15
- aiteamutils-0.2.50.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- aiteamutils-0.2.50.dist-info/RECORD,,