ipulse-shared-core-ftredge 20.0.1__tar.gz → 23.1.1__tar.gz

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.

Potentially problematic release.


This version of ipulse-shared-core-ftredge might be problematic. Click here for more details.

Files changed (70) hide show
  1. {ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-23.1.1}/PKG-INFO +3 -4
  2. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/setup.py +3 -4
  3. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/cache/shared_cache.py +1 -2
  4. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +96 -0
  5. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +128 -157
  6. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/exceptions/base_exceptions.py +35 -4
  7. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +3 -7
  8. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
  9. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
  10. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +274 -0
  11. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/models/catalog/usertype.py +177 -0
  12. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
  13. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
  14. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/models/user/user_subscription.py +348 -0
  15. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/models/user_auth.py → ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/models/user/userauth.py +19 -10
  16. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/models/user_profile.py → ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/models/user/userprofile.py +53 -21
  17. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/models/user/userstatus.py +479 -0
  18. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/monitoring/__init__.py +0 -2
  19. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/monitoring/tracemon.py +6 -6
  20. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/__init__.py +23 -0
  21. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
  22. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/services/base/base_firestore_service.py +77 -16
  23. {ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services → ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/base}/cache_aware_firestore_service.py +46 -32
  24. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
  25. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +277 -0
  26. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +376 -0
  27. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
  28. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/user/__init__.py +17 -0
  29. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +677 -0
  30. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +796 -0
  31. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
  32. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +488 -0
  33. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/user/userauth_operations.py +928 -0
  34. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/user/userprofile_operations.py +166 -0
  35. ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/user/userstatus_operations.py +476 -0
  36. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/charging_service.py → ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge/services/user_charging_service.py +9 -9
  37. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +3 -4
  38. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +20 -13
  39. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +1 -2
  40. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -59
  41. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/models/subscription.py +0 -190
  42. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/models/user_status.py +0 -495
  43. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -526
  44. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/__init__.py +0 -25
  45. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/__init__.py +0 -37
  46. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
  47. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
  48. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
  49. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
  50. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +0 -651
  51. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
  52. ipulse_shared_core_ftredge-20.0.1/tests/test_cache_aware_service.py +0 -270
  53. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/LICENCE +0 -0
  54. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/README.md +0 -0
  55. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/pyproject.toml +0 -0
  56. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/setup.cfg +0 -0
  57. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/__init__.py +0 -0
  58. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
  59. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  60. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
  61. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
  62. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/exceptions/__init__.py +0 -0
  63. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py +0 -0
  64. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +0 -0
  65. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
  66. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
  67. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
  68. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  69. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
  70. {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-23.1.1}/tests/test_shared_cache.py +0 -0
@@ -1,19 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 20.0.1
3
+ Version: 23.1.1
4
4
  Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
5
5
  Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
6
6
  Author: Russlan Ramdowar
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Operating System :: OS Independent
10
- Requires-Python: >=3.11
10
+ Requires-Python: >=3.12
11
11
  License-File: LICENCE
12
12
  Requires-Dist: pydantic[email]~=2.5
13
13
  Requires-Dist: python-dateutil~=2.8
14
14
  Requires-Dist: fastapi~=0.115.8
15
- Requires-Dist: pytest
16
- Requires-Dist: ipulse_shared_base_ftredge==7.2.0
15
+ Requires-Dist: ipulse_shared_base_ftredge==10.2.1
17
16
  Dynamic: author
18
17
  Dynamic: classifier
19
18
  Dynamic: home-page
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
3
3
 
4
4
  setup(
5
5
  name='ipulse_shared_core_ftredge',
6
- version='20.0.1',
6
+ version='23.1.1',
7
7
  package_dir={'': 'src'}, # Specify the source directory
8
8
  packages=find_packages(where='src'), # Look for packages in 'src'
9
9
  install_requires=[
@@ -11,8 +11,7 @@ setup(
11
11
  'pydantic[email]~=2.5',
12
12
  'python-dateutil~=2.8',
13
13
  'fastapi~=0.115.8',
14
- 'pytest',
15
- 'ipulse_shared_base_ftredge==7.2.0',
14
+ 'ipulse_shared_base_ftredge==10.2.1',
16
15
  ],
17
16
  author='Russlan Ramdowar',
18
17
  description='Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.',
@@ -23,5 +22,5 @@ setup(
23
22
  'License :: OSI Approved :: MIT License',
24
23
  'Operating System :: OS Independent',
25
24
  ],
26
- python_requires='>=3.11',
25
+ python_requires='>=3.12',
27
26
  )
@@ -1,12 +1,11 @@
1
1
  """Module for shared caching functionality that can be used across microservices."""
2
- import os
3
2
  import time
4
3
  import logging
5
4
  import traceback
6
5
  import inspect
7
6
  import asyncio
8
7
  import threading
9
- from typing import Dict, Any, Optional, TypeVar, Generic, Callable, Tuple, List, Awaitable
8
+ from typing import Dict, Any, Optional, TypeVar, Generic, Callable, Tuple,Awaitable
10
9
 
11
10
  T = TypeVar('T')
12
11
 
@@ -0,0 +1,96 @@
1
+ from typing import Optional, Annotated
2
+ from fastapi import Request, HTTPException, Depends, Header
3
+ from firebase_admin import auth
4
+ from ..models.user.userauth import UserAuth
5
+
6
+ async def verify_firebase_token(
7
+ request: Request,
8
+ x_forwarded_authorization: Optional[str] = Header(None),
9
+ authorization: Optional[str] = Header(None)
10
+ ) -> UserAuth:
11
+ """
12
+ Verify Firebase ID token and return a UserAuth instance with authenticated user data.
13
+
14
+ Args:
15
+ request: FastAPI request object
16
+ x_forwarded_authorization: Authorization header from proxy/load balancer
17
+ authorization: Standard authorization header
18
+
19
+ Returns:
20
+ UserAuth instance with verified user data
21
+
22
+ Raises:
23
+ HTTPException: 401 if token is missing or invalid
24
+ """
25
+ # Get token from either x-forwarded-authorization or authorization header
26
+ token = x_forwarded_authorization or authorization
27
+
28
+ if not token or token == "None" or not token.strip():
29
+ raise HTTPException(
30
+ status_code=401,
31
+ detail="Authorization token is missing"
32
+ )
33
+
34
+ try:
35
+ # Handle potential Header object or string
36
+ if hasattr(token, '__str__') and not isinstance(token, str):
37
+ # If it's a Header object or similar, convert to string
38
+ token_str = str(token)
39
+ # Check if the string representation looks like a Header object's repr
40
+ if 'annotation=' in token_str or 'required=' in token_str:
41
+ # This means we got the internal representation of a Header object
42
+ # In this case, we need to extract the actual value
43
+ if hasattr(token, 'default'):
44
+ token_str = str(token.default) if token.default is not None else ""
45
+ else:
46
+ token_str = ""
47
+ elif token_str == "None":
48
+ token_str = ""
49
+ else:
50
+ # It's already a string
51
+ token_str = token if isinstance(token, str) else str(token)
52
+
53
+ # Check if we still have an empty or None token
54
+ if not token_str or token_str == "None":
55
+ raise HTTPException(
56
+ status_code=401,
57
+ detail="Authorization token is missing"
58
+ )
59
+
60
+ # Remove 'Bearer ' prefix if present
61
+ token_str = token_str.replace("Bearer ", "")
62
+
63
+ # Verify the token
64
+ decoded_token = auth.verify_id_token(token_str)
65
+
66
+ # Create UserAuth instance from decoded token
67
+ email = decoded_token.get('email')
68
+ if not email:
69
+ raise ValueError("Token must contain user email")
70
+
71
+ user_auth = UserAuth(
72
+ email=email,
73
+ password=None, # No password for token-based auth
74
+ display_name=decoded_token.get('name'),
75
+ firebase_uid=decoded_token.get('uid'),
76
+ email_verified=decoded_token.get('email_verified', False),
77
+ custom_claims=decoded_token.get('custom_claims', {}),
78
+ phone_number=decoded_token.get('phone_number'),
79
+ # Note: provider_data, metadata, and timestamps would come from getUserRecord() if needed
80
+ )
81
+
82
+ # Store the full decoded token in request state for use in authorization middleware
83
+ # This maintains backward compatibility with existing authorization code
84
+ request.state.user = decoded_token
85
+
86
+ return user_auth
87
+
88
+ except Exception as e:
89
+ raise HTTPException(
90
+ status_code=401,
91
+ detail=f"Invalid token: {str(e)}"
92
+ ) from e
93
+
94
+ # Type alias for dependency injection
95
+ AuthUser = Annotated[UserAuth, Depends(verify_firebase_token)]
96
+
@@ -1,55 +1,52 @@
1
1
  import os
2
2
  import logging
3
- import json
4
3
  import asyncio
5
- from concurrent.futures import ThreadPoolExecutor
6
4
  from typing import Optional, Iterable, Dict, Any, List
7
5
  from datetime import datetime, timedelta, timezone
8
- import json
9
6
  import httpx
10
7
  from fastapi import HTTPException, Request
11
- from google.cloud import firestore
12
- from ipulse_shared_core_ftredge.services import ServiceError, AuthorizationError, ResourceNotFoundError
8
+ from ipulse_shared_core_ftredge.exceptions import ServiceError, ResourceNotFoundError
13
9
  from ipulse_shared_core_ftredge.models import UserStatus
14
- from ipulse_shared_core_ftredge.utils.json_encoder import convert_to_json_serializable
10
+ from ipulse_shared_core_ftredge.services import UserCoreService
11
+ from ipulse_shared_base_ftredge import ApprovalStatus
15
12
 
16
- # Constants derived from UserStatus model
17
- USERS_STATUS_COLLECTION_NAME = UserStatus.COLLECTION_NAME
18
- USERS_STATUS_DOC_REF = f"{UserStatus.OBJ_REF}_" # Use OBJ_REF and append underscore
13
+ # Cache TTL constant
19
14
  USERSTATUS_CACHE_TTL = 60 # 60 seconds
20
15
 
21
16
  class UserStatusCache:
22
17
  """Manages user status caching with dynamic invalidation"""
23
18
  def __init__(self):
24
- self._cache: Dict[str, Dict[str, Any]] = {}
19
+ self._cache: Dict[str, UserStatus] = {}
25
20
  self._timestamps: Dict[str, datetime] = {}
26
21
 
27
- def get(self, user_uid: str) -> Optional[Dict[str, Any]]:
22
+ def get(self, user_uid: str) -> Optional[UserStatus]:
28
23
  """
29
24
  Retrieves user status from cache if available and valid.
30
25
 
31
26
  Args:
32
27
  user_uid (str): The user ID.
33
28
 
29
+ Returns:
30
+ UserStatus object if cached and valid, None otherwise
34
31
  """
35
32
  if user_uid in self._cache:
36
- status_data = self._cache[user_uid]
33
+ status_obj = self._cache[user_uid]
37
34
  # Force refresh for credit-consuming or sensitive operations
38
35
  # Check TTL for normal operations
39
36
  if datetime.now() - self._timestamps[user_uid] < timedelta(seconds=USERSTATUS_CACHE_TTL):
40
- return status_data
37
+ return status_obj
41
38
  self.invalidate(user_uid)
42
39
  return None
43
40
 
44
- def set(self, user_uid: str, data: Dict[str, Any]) -> None:
41
+ def set(self, user_uid: str, status: UserStatus) -> None:
45
42
  """
46
- Sets user status data in the cache.
43
+ Sets user status object in the cache.
47
44
 
48
45
  Args:
49
46
  user_uid (str): The user ID.
50
- data (Dict[str, Any]): The user status data to cache.
47
+ status (UserStatus): The user status object to cache.
51
48
  """
52
- self._cache[user_uid] = data
49
+ self._cache[user_uid] = status
53
50
  self._timestamps[user_uid] = datetime.now()
54
51
 
55
52
  def invalidate(self, user_uid: str) -> None:
@@ -68,61 +65,26 @@ userstatus_cache = UserStatusCache()
68
65
  # Replace the logger dependency with a standard logger
69
66
  logger = logging.getLogger(__name__)
70
67
 
71
- # Create a custom FirestoreTimeoutError class that can be identified in middlewares
72
- class FirestoreTimeoutError(TimeoutError):
73
- """Custom exception for Firestore timeout errors to make them more identifiable."""
74
- pass
75
-
76
-
77
- # Define a function to get a Firestore document with a strict timeout
78
- async def get_with_strict_timeout(doc_ref, timeout_seconds: float):
79
- """
80
- Get a Firestore document with a strictly enforced timeout.
81
-
82
- Args:
83
- doc_ref: Firestore document reference
84
- timeout_seconds: Maximum time to wait in seconds
85
-
86
- Returns:
87
- Document snapshot
88
-
89
- Raises:
90
- FirestoreTimeoutError: If the operation takes longer than timeout_seconds
91
- """
92
- loop = asyncio.get_running_loop()
93
- with ThreadPoolExecutor() as executor:
94
- try:
95
- # Run the blocking Firestore get() operation in a thread and apply a strict timeout
96
- logger.debug(f"Starting Firestore get with strict timeout of {timeout_seconds}s")
97
- return await asyncio.wait_for(
98
- loop.run_in_executor(executor, doc_ref.get),
99
- timeout=timeout_seconds
100
- )
101
- except asyncio.TimeoutError:
102
- error_message = f"User Status fetching for Authz timed out after {timeout_seconds} seconds, perhaps issue with Firestore Connectivity"
103
- logger.error(error_message)
104
- raise FirestoreTimeoutError(error_message)
105
-
106
- # Update get_userstatus to use our new strict timeout function
107
68
  async def get_userstatus(
108
69
  user_uid: str,
109
- db: firestore.Client,
110
- force_fresh: bool = False,
111
- timeout: float = 12.0 # Default timeout but allow override
112
- ) -> tuple[Dict[str, Any], bool]:
70
+ user_core_service: UserCoreService,
71
+ force_fresh: bool = False
72
+ ) -> tuple[UserStatus, bool]:
113
73
  """
114
- Fetch user status with intelligent caching and configurable timeout
74
+ Lightweight fetch of user status with caching.
75
+ Returns UserStatus objects directly for better performance.
115
76
 
116
77
  Args:
117
78
  user_uid: User ID to fetch status for
118
- db: Firestore client
79
+ user_core_service: UserCoreService for data retrieval
119
80
  force_fresh: Whether to bypass cache
120
- timeout: Timeout for Firestore operations in seconds
121
81
 
122
82
  Returns:
123
- Tuple of (user status data, whether cache was used)
83
+ Tuple of (UserStatus object, whether cache was used)
124
84
  """
125
85
  cache_used = False
86
+
87
+ # Check cache first unless forced fresh
126
88
  if not force_fresh:
127
89
  cached_status = userstatus_cache.get(user_uid)
128
90
  if cached_status:
@@ -130,60 +92,37 @@ async def get_userstatus(
130
92
  return cached_status, cache_used
131
93
 
132
94
  try:
133
- # Get reference to the document
134
- userstatus_id = USERS_STATUS_DOC_REF + user_uid
135
- user_ref = db.collection(USERS_STATUS_COLLECTION_NAME).document(userstatus_id)
136
-
137
- logger.debug(f"Fetching user status for {user_uid} with strict timeout {timeout}s")
138
-
139
- # Use our strict timeout wrapper instead of the native timeout parameter
140
- snapshot = await get_with_strict_timeout(user_ref, timeout)
141
-
142
- if not snapshot.exists:
143
- # Log at DEBUG level since this might be expected for new users
144
- logger.debug(f"User status document not found for user {user_uid} (document: {userstatus_id})")
95
+ status_obj = await user_core_service.get_userstatus(user_uid)
96
+ if not status_obj:
145
97
  raise ResourceNotFoundError(
146
- resource_type="authz_for_apis>userstatus",
147
- resource_id=userstatus_id,
148
- additional_info={"user_uid": user_uid, "context": "authorization"}
98
+ resource_type="UserStatus",
99
+ resource_id=user_uid,
100
+ additional_info={"message": "User status not found"}
149
101
  )
150
102
 
151
- status_data = snapshot.to_dict()
152
-
153
- # Only cache if not forced fresh
154
- if not force_fresh:
155
- userstatus_cache.set(user_uid, status_data)
156
- return status_data, cache_used
103
+ # Ensure we have a UserStatus object
104
+ if not isinstance(status_obj, UserStatus):
105
+ # If it's a dict, convert to UserStatus
106
+ if isinstance(status_obj, dict):
107
+ status_obj = UserStatus(**status_obj)
108
+ else:
109
+ raise ValueError(f"Expected UserStatus object or dict, got {type(status_obj)}")
157
110
 
158
- except ResourceNotFoundError:
159
- # Re-raise ResourceNotFoundError as-is - don't wrap in ServiceError
160
- raise
161
- except (TimeoutError, FirestoreTimeoutError) as e:
162
- logger.error(f"Timeout while fetching user status for {user_uid}: {str(e)}")
163
- raise ServiceError(
164
- operation="fetching user status for authz",
165
- error=e,
166
- resource_type="userstatus",
167
- resource_id=user_uid,
168
- additional_info={
169
- "force_fresh": force_fresh,
170
- "collection": USERS_STATUS_COLLECTION_NAME,
171
- "timeout_seconds": timeout
172
- }
173
- )
174
111
  except Exception as e:
175
- logger.error(f"Error fetching user status for {user_uid}: {str(e)}")
112
+ logger.error(f"Error fetching user status via UserCoreService: {str(e)}")
176
113
  raise ServiceError(
177
- operation=f"fetching user status",
114
+ operation="fetching user status for authz via UserCoreService",
178
115
  error=e,
179
- resource_type="userstatus",
180
- resource_id=user_uid,
181
- additional_info={
182
- "force_fresh": force_fresh,
183
- "collection": USERS_STATUS_COLLECTION_NAME
184
- }
116
+ resource_type="UserStatus",
117
+ resource_id=user_uid
185
118
  ) from e
186
119
 
120
+ # Cache the UserStatus object
121
+ if not force_fresh:
122
+ userstatus_cache.set(user_uid, status_obj)
123
+
124
+ return status_obj, cache_used
125
+
187
126
  def _validate_resource_fields(fields: Dict[str, Any]) -> List[str]:
188
127
  """
189
128
  Filter out invalid fields similar to BaseFirestoreService validation.
@@ -228,19 +167,18 @@ async def extract_request_fields(request: Request) -> Optional[List[str]]:
228
167
  # Main authorization function with configurable timeout
229
168
  async def authorizeAPIRequest(
230
169
  request: Request,
231
- db: firestore.Client,
170
+ user_core_service: UserCoreService, # UserCoreService instance for better integration
232
171
  request_resource_fields: Optional[Iterable[str]] = None,
233
- firestore_timeout: float = 15.0 # Allow specifying timeout
172
+
234
173
  ) -> Dict[str, Any]:
235
174
  """
236
175
  Authorize API request based on user status and OPA policies.
237
- Enhanced with credit check information and proper exception handling.
176
+ Enhanced to use UserCoreService when available and fetch usertype from Firebase custom claims.
238
177
 
239
178
  Args:
240
179
  request: The incoming request
241
- db: Firestore client
180
+ user_core_service: UserCoreService instance for better integration
242
181
  request_resource_fields: Fields being accessed/modified in the request
243
- firestore_timeout: Timeout for Firestore operations in seconds
244
182
 
245
183
  Returns:
246
184
  Authorization result containing decision details
@@ -254,34 +192,76 @@ async def authorizeAPIRequest(
254
192
  if not request_resource_fields:
255
193
  request_resource_fields = await extract_request_fields(request)
256
194
 
257
- # Extract request context
258
- user_uid = request.state.user.get('uid')
195
+ # Extract request context and Firebase user claims
196
+ firebase_user = request.state.user
197
+ user_uid = firebase_user.get('uid')
259
198
  if not user_uid:
260
- # Log authorization failures at DEBUG level, not ERROR
261
199
  logger.debug(f"Authorization denied for {request.method} {request.url.path}: No user UID found")
262
200
  raise HTTPException(
263
201
  status_code=403,
264
202
  detail="Not authorized to access this resource"
265
203
  )
266
204
 
267
- # Determine if we need fresh status
205
+ # Get usertype information from Firebase custom claims (primary source)
206
+ custom_claims = firebase_user.get('custom_claims', {}) or firebase_user.get('claims', {})
207
+ primary_usertype = custom_claims.get('primary_usertype')
208
+ secondary_usertypes = custom_claims.get('secondary_usertypes', [])
209
+ user_approval_status = custom_claims.get('user_approval_status', str(ApprovalStatus.UNKNOWN))
210
+
211
+ # Determine if we need fresh status for permissions and credits
268
212
  force_fresh = _should_force_fresh_status(request)
269
- userstatus, cache_used = await get_userstatus(
270
- user_uid,
271
- db,
213
+ user_status_obj, cache_used = await get_userstatus(
214
+ user_uid=user_uid,
215
+ user_core_service=user_core_service,
272
216
  force_fresh=force_fresh,
273
- timeout=firestore_timeout # Pass the specified timeout
274
217
  )
275
218
 
276
- # Prepare authorization input that matches OPA expectations
277
- # Extract required values from user status
278
- primary_usertype = userstatus.get("primary_usertype")
279
- secondary_usertypes = userstatus.get("secondary_usertypes", [])
219
+ # Perform comprehensive review and cleanup synchronously to ensure accurate auth data
220
+ if user_core_service:
221
+ try:
222
+ review_result = await user_core_service.review_and_clean_active_subscription_credits_and_permissions(
223
+ user_uid=user_uid,
224
+ updater_uid="Auto-AuthzMiddlewareDependency",
225
+ review_auto_renewal=True,
226
+ apply_fallback=True,
227
+ clean_expired_permissions=True,
228
+ review_credits=True
229
+ )
230
+
231
+ # Refresh user status after comprehensive review if any actions were taken
232
+ if review_result.get('actions_taken'):
233
+ logger.info(f"Auth middleware performed comprehensive review for user {user_uid}: {review_result['actions_taken']}")
234
+ # Use the updated UserStatus returned from the review function
235
+ if 'updated_userstatus' in review_result:
236
+ user_status_obj = review_result['updated_userstatus']
237
+ # Invalidate cache since we updated the user status
238
+ userstatus_cache.invalidate(user_uid)
239
+ # Update cache with the new status
240
+ userstatus_cache.set(user_uid, user_status_obj)
241
+
242
+ except Exception as e:
243
+ logger.warning(f"Comprehensive review failed for user {user_uid} during auth: {str(e)}")
244
+ # Continue with existing status if review fails
245
+
246
+ # Get valid permissions after comprehensive review
247
+ valid_permissions_objs = user_status_obj.get_valid_permissions()
248
+
249
+ # Convert UserPermission objects to minimal serializable format for OPA
250
+ valid_permissions = [
251
+ {
252
+ "domain": perm.domain,
253
+ "iam_unit_type": str(perm.iam_unit_type), # Convert enum to string
254
+ "permission_ref": perm.permission_ref
255
+ }
256
+ for perm in valid_permissions_objs
257
+ ]
280
258
 
281
- # Extract IAM domain permissions
282
- iam_domain_permissions = userstatus.get("iam_domain_permissions", {})
259
+ # Extract active subscription plan ID
260
+ active_subscription_plan_id = None
261
+ if user_status_obj.active_subscription is not None:
262
+ active_subscription_plan_id = user_status_obj.active_subscription.plan_id
283
263
 
284
- # Format the authz_input to match what the OPA policies expect
264
+ # Format the authz_input for OPA (optimized for speed)
285
265
  authz_input = {
286
266
  "api_url": request.url.path,
287
267
  "requestor": {
@@ -289,39 +269,34 @@ async def authorizeAPIRequest(
289
269
  "primary_usertype": primary_usertype,
290
270
  "secondary_usertypes": secondary_usertypes,
291
271
  "usertypes": [primary_usertype] + secondary_usertypes if primary_usertype else secondary_usertypes,
292
- "email_verified": request.state.user.get("email_verified", False),
293
- "iam_domain_permissions": iam_domain_permissions,
294
- "sbscrptn_based_insight_credits": userstatus.get("sbscrptn_based_insight_credits", 0),
295
- "extra_insight_credits": userstatus.get("extra_insight_credits", 0)
272
+ "email_verified": firebase_user.get("email_verified", False),
273
+ "user_approval_status": user_approval_status,
274
+ "iam_permissions": valid_permissions,
275
+ "sbscrptn_based_insight_credits": user_status_obj.sbscrptn_based_insight_credits or 0,
276
+ "extra_insight_credits": user_status_obj.extra_insight_credits or 0,
277
+ "active_subscription_plan_id": active_subscription_plan_id
296
278
  },
297
279
  "method": request.method.lower(),
298
280
  "request_resource_fields": request_resource_fields
299
281
  }
300
282
 
301
- # Convert any non-serializable objects to JSON serializable format
302
- # Using the unified utility from utils
303
- json_safe_authz_input = convert_to_json_serializable(authz_input)
283
+ # PERFORMANCE OPTIMIZATION: Skip convert_to_json_serializable() and json.dumps()
284
+ # The authz_input structure above contains only JSON-safe types:
285
+ # - strings, integers, booleans, lists of strings, and simple dicts
286
+ # - No datetime objects, enums, or complex Pydantic models
287
+ # - httpx.post(json=...) handles serialization efficiently
288
+ # This saves ~2-5ms per request on a high-frequency auth endpoint
304
289
 
305
- # Query OPA
290
+ # Query OPA (optimized for speed - no unnecessary serialization)
306
291
  opa_url = f"{os.getenv('OPA_SERVER_URL', 'http://localhost:8181')}{os.getenv('OPA_DECISION_PATH', '/v1/data/http/authz/ingress/decision')}"
307
- logger.debug(f"Attempting to connect to OPA at: {opa_url}")
308
-
309
- # Debug: Print raw JSON payload to identify any potential issues
310
- try:
311
- payload_json = json.dumps({"input": json_safe_authz_input})
312
- logger.debug(f"OPA Request JSON payload: {payload_json}")
313
- except Exception as json_err:
314
- logger.error(f"Error serializing OPA request payload: {json_err}")
315
292
 
316
293
  async with httpx.AsyncClient() as client:
317
294
  try:
318
295
  response = await client.post(
319
296
  opa_url,
320
- json={"input": json_safe_authz_input},
321
- timeout=5.0 # 5 seconds timeout
297
+ json={"input": authz_input},
298
+ timeout=5.0
322
299
  )
323
- logger.debug(f"OPA Response Status: {response.status_code}")
324
- # logger.debug(f"OPA Response Body: {response.text}")
325
300
 
326
301
  if response.status_code != 200:
327
302
  logger.error(f"OPA authorization failed: {response.text}")
@@ -331,36 +306,32 @@ async def authorizeAPIRequest(
331
306
  )
332
307
 
333
308
  result = response.json()
334
- logger.debug(f"Parsed OPA response: {result}")
335
309
 
336
- # Handle unusual OPA response formats
337
- if "result" in result:
338
- opa_decision = result["result"]
339
- else:
340
- logger.warning(f"OPA response missing 'result' field, using default")
310
+ # Handle OPA response format
311
+ if "result" not in result:
312
+ logger.warning("OPA response missing 'result' field")
341
313
  raise HTTPException(
342
314
  status_code=500,
343
315
  detail="Authorization service error: OPA response format unexpected"
344
316
  )
345
317
 
346
- # Extract key fields from result with better default handling
318
+ opa_decision = result["result"]
347
319
  allow = opa_decision.get("allow", False)
348
320
 
349
- # Handle authorization denial - log at DEBUG level, not ERROR
321
+ # Handle authorization denial
350
322
  if not allow:
351
- logger.debug(f"Authorization denied for {request.method} {request.url.path}: insufficient permissions")
323
+ logger.debug(f"Authorization denied for {request.method} {request.url.path}")
352
324
  raise HTTPException(
353
325
  status_code=403,
354
326
  detail=f"Not authorized to {request.method} {request.url.path}"
355
327
  )
356
328
 
357
329
  except httpx.RequestError as e:
358
- # Only log actual system errors at ERROR level
359
330
  logger.error(f"Failed to connect to OPA: {str(e)}")
360
331
  raise HTTPException(
361
332
  status_code=500,
362
333
  detail="Authorization service temporarily unavailable"
363
- )
334
+ ) from e
364
335
 
365
336
  # More descriptive metadata about the data freshness
366
337
  return {
@@ -21,7 +21,7 @@ class BaseServiceException(HTTPException):
21
21
  self.original_error = original_error
22
22
 
23
23
  # Get full traceback if there's an original error
24
- if original_error:
24
+ if original_error and hasattr(original_error, '__traceback__'):
25
25
  self.traceback = ''.join(traceback.format_exception(
26
26
  type(original_error),
27
27
  original_error,
@@ -69,18 +69,26 @@ class ServiceError(BaseServiceException):
69
69
  def __init__(
70
70
  self,
71
71
  operation: str,
72
- error: Exception,
72
+ error: Any, # Allow string or exception
73
73
  resource_type: str,
74
74
  resource_id: Optional[str] = None,
75
75
  additional_info: Optional[Dict[str, Any]] = None
76
76
  ):
77
+ # If a string is passed as an error, wrap it in a generic Exception
78
+ if isinstance(error, str):
79
+ original_error = Exception(error)
80
+ error_detail = error
81
+ else:
82
+ original_error = error
83
+ error_detail = str(error)
84
+
77
85
  super().__init__(
78
86
  status_code=500,
79
- detail=f"Error during {operation}: {str(error)}",
87
+ detail=f"Error during {operation}: {error_detail}",
80
88
  resource_type=resource_type,
81
89
  resource_id=resource_id,
82
90
  additional_info=additional_info,
83
- original_error=error
91
+ original_error=original_error
84
92
  )
85
93
 
86
94
 
@@ -133,3 +141,26 @@ class ValidationError(BaseServiceException):
133
141
  resource_id=resource_id,
134
142
  additional_info=additional_info
135
143
  )
144
+
145
+
146
+ class ConfigurationError(BaseServiceException):
147
+ def __init__(
148
+ self,
149
+ detail: str,
150
+ resource_type: str = "configuration",
151
+ resource_id: Optional[str] = None,
152
+ operation: Optional[str] = None,
153
+ additional_info: Optional[Dict[str, Any]] = None,
154
+ original_error: Optional[Exception] = None
155
+ ):
156
+ if operation:
157
+ detail = f"{detail} (Operation: {operation})"
158
+
159
+ super().__init__(
160
+ status_code=500,
161
+ detail=detail,
162
+ resource_type=resource_type,
163
+ resource_id=resource_id,
164
+ additional_info=additional_info,
165
+ original_error=original_error
166
+ )