aiteamutils 0.2.51__py3-none-any.whl → 0.2.52__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
@@ -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.51"
2
+ __version__ = "0.2.52"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.51
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=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,,