abs-auth-rbac-core 0.1.8__py3-none-any.whl → 0.3.18__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.
@@ -1,3 +1,15 @@
1
1
  from .jwt_functions import JWTFunctions
2
+ from .middleware import (
3
+ CustomHTTPBearer,
4
+ auth_middleware,
5
+ contacts_auth_middleware,
6
+ unified_auth_middleware
7
+ )
2
8
 
3
- __all__ = ["JWTFunctions"]
9
+ __all__ = [
10
+ "JWTFunctions",
11
+ "CustomHTTPBearer",
12
+ "auth_middleware",
13
+ "contacts_auth_middleware",
14
+ "unified_auth_middleware"
15
+ ]
@@ -20,7 +20,10 @@ def get_user_by_attribute(db_session: Callable[...,Any],attribute: str, value: s
20
20
  if not hasattr(Users, attribute):
21
21
  raise ValidationError(detail=f"Attribute {attribute} does not exist on the User model")
22
22
 
23
- user = session.query(Users).filter(getattr(Users, attribute) == value).first()
23
+ user = session.query(Users).filter(
24
+ getattr(Users, attribute) == value,
25
+ Users.deleted_at.is_(None) # Filter out soft-deleted users
26
+ ).first()
24
27
 
25
28
  if not user:
26
29
  raise NotFoundError(detail="User not found")
@@ -1,13 +1,39 @@
1
1
  from fastapi import Depends, Request
2
2
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+ from fastapi import HTTPException
3
4
  import logging
4
- from typing import Callable, Any
5
+ from typing import Callable, Any, Optional
5
6
 
6
7
  from .jwt_functions import JWTFunctions
7
8
  from .auth_functions import get_user_by_attribute
8
- from abs_exception_core.exceptions import UnauthorizedError
9
+ from abs_exception_core.exceptions import UnauthorizedError, AuthError, NotFoundError
10
+ from abs_nosql_repository_core.repository import BaseRepository
11
+ from fastapi.security.utils import get_authorization_scheme_param
9
12
 
10
- security = HTTPBearer()
13
+ class CustomHTTPBearer(HTTPBearer):
14
+ def __init__(self, **kwargs):
15
+ super().__init__(**kwargs)
16
+
17
+ async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]:
18
+ authorization = request.headers.get("Authorization")
19
+ scheme, credentials = get_authorization_scheme_param(authorization)
20
+
21
+ if not (authorization and scheme and credentials):
22
+ if self.auto_error:
23
+ raise UnauthorizedError(detail="Invalid authentication credentials")
24
+ else:
25
+ return None
26
+
27
+ if scheme.lower() != "bearer":
28
+ if self.auto_error:
29
+ raise UnauthorizedError(detail="Invalid authentication credentials")
30
+ else:
31
+ return None
32
+
33
+ return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
34
+
35
+ security = CustomHTTPBearer()
36
+ # security = HTTPBearer()
11
37
  logger = logging.getLogger(__name__)
12
38
 
13
39
 
@@ -45,7 +71,175 @@ def auth_middleware(
45
71
  request.state.user = user
46
72
  return user
47
73
 
74
+ except UnauthorizedError as e:
75
+ logger.error(e)
76
+ raise
48
77
  except Exception as e:
49
78
  logger.error(f"Authentication error: {str(e)}", exc_info=True)
50
79
  raise UnauthorizedError(detail="Authentication failed")
80
+
81
+ return get_auth
82
+
83
+
84
+ def contacts_auth_middleware(
85
+ entity_records_service,
86
+ jwt_secret_key: str,
87
+ jwt_algorithm: str
88
+ ):
89
+ """
90
+ Contact authentication middleware using EntityRecordsService.
91
+
92
+ This middleware validates contact JWT tokens and attaches contact data to request.state.
93
+
94
+ JWT Payload Structure:
95
+ {
96
+ "sub": "contact@example.com",
97
+ "cid": "contact_record_id", # KEY IDENTIFIER for contacts
98
+ "uuid": "unique-uuid",
99
+ "exp": 1234567890,
100
+ "iat": 1234567890
101
+ }
102
+
103
+ Args:
104
+ entity_records_service: EntityRecordsService for entity operations
105
+ jwt_secret_key: JWT secret key
106
+ jwt_algorithm: JWT algorithm (e.g., HS256)
107
+
108
+ Returns:
109
+ FastAPI dependency function that validates contact authentication
110
+ """
111
+
112
+ async def get_auth(
113
+ request: Request,
114
+ token: HTTPAuthorizationCredentials = Depends(security)
115
+ ):
116
+ jwt_functions = JWTFunctions(
117
+ secret_key=jwt_secret_key,
118
+ algorithm=jwt_algorithm
119
+ )
120
+
121
+ try:
122
+ if not token or not token.credentials:
123
+ raise UnauthorizedError(detail="Invalid authentication credentials")
124
+
125
+ # Decode JWT and extract payload
126
+ payload = jwt_functions.get_data(token=token.credentials)
127
+
128
+ # Extract cid (required for contact authentication)
129
+ cid = payload.get("cid")
130
+ if not cid:
131
+ raise UnauthorizedError(detail="Invalid contact token")
132
+
133
+ # Extract app_id from path parameters
134
+ app_id = request.path_params.get("app_id")
135
+ if not app_id:
136
+ raise UnauthorizedError(detail="App ID is required")
137
+
138
+ logger.info(f"Contact auth: app_id={app_id}, cid={cid}")
139
+
140
+ # Get app configuration (apps collection is not an entity collection)
141
+ # We still need BaseRepository for this until apps get moved to a service
142
+ base_repo = BaseRepository(db=entity_records_service.repository.db)
143
+ app = await base_repo.get_by_attr("id", app_id, collection_name="apps")
144
+ if not app:
145
+ logger.error(f"App not found: {app_id}")
146
+ raise UnauthorizedError(detail="App not found")
147
+
148
+ # Extract contact entity ID from app metadata
149
+ metadata = app.get("metadata", {})
150
+ if not metadata:
151
+ raise UnauthorizedError(detail="App doesn't have support for contacts")
152
+
153
+ features_entities = metadata.get("features_entities", {})
154
+ if "CONTACT" not in features_entities:
155
+ raise UnauthorizedError(detail="App doesn't have support for contacts")
156
+
157
+ contact_config = features_entities.get("CONTACT", {})
158
+ contact_entity_id = contact_config.get("Contacts")
159
+
160
+ if not contact_entity_id:
161
+ raise UnauthorizedError(detail="Contact entity not configured")
162
+
163
+ logger.info(f"Contact entity ID: {contact_entity_id}")
164
+
165
+ # Get contact record using EntityRecordsService
166
+ try:
167
+ contact = await entity_records_service.get_record_by_id(
168
+ entity_id=contact_entity_id,
169
+ record_id=cid,
170
+ convert_ids=False # Keep ObjectIds for internal use
171
+ )
172
+ except NotFoundError:
173
+ logger.error(f"Authentication failed: contact with id {cid} not found")
174
+ raise UnauthorizedError(detail="Authentication failed")
175
+
176
+ logger.info(f"✅ Contact authenticated: {cid}")
177
+
178
+ # Attach contact to request state
179
+ request.state.contact = contact
180
+ request.state.contact_entity_id = contact_entity_id
181
+
182
+ return contact
183
+
184
+ except UnauthorizedError:
185
+ raise
186
+ except Exception as e:
187
+ logger.error(f"Authentication error: {str(e)}", exc_info=True)
188
+ raise UnauthorizedError(detail="Authentication failed")
189
+
190
+ return get_auth
191
+
192
+
193
+ def unified_auth_middleware(
194
+ jwt_secret_key: str,
195
+ jwt_algorithm: str,
196
+ get_user_auth_middleware: Callable,
197
+ get_contact_auth_middleware: Callable,
198
+ ):
199
+ """
200
+ Unified authentication middleware that intelligently routes to the correct
201
+ authentication flow based on JWT payload.
202
+
203
+ Detection Logic:
204
+ - If JWT contains "cid" field → Contact authentication
205
+ - Otherwise → User authentication
206
+
207
+ Benefits:
208
+ - Single entry point for all authentication
209
+ - JWT decoded only once for efficiency
210
+ - Transparent routing based on token type
211
+ - No code changes needed in route handlers
212
+
213
+ Args:
214
+ jwt_secret_key: JWT secret key
215
+ jwt_algorithm: JWT algorithm (e.g., HS256)
216
+ get_user_auth_middleware: User auth middleware callable
217
+ get_contact_auth_middleware: Contact auth middleware callable
218
+
219
+ Returns:
220
+ FastAPI dependency that handles unified authentication
221
+ """
222
+ async def get_auth(
223
+ request: Request,
224
+ token: HTTPAuthorizationCredentials = Depends(security)
225
+ ):
226
+ if not token or not token.credentials:
227
+ raise UnauthorizedError(detail="Invalid authentication credentials")
228
+
229
+ # Decode JWT once to inspect payload
230
+ jwt_functions = JWTFunctions(
231
+ secret_key=jwt_secret_key,
232
+ algorithm=jwt_algorithm
233
+ )
234
+ payload = jwt_functions.get_data(token=token.credentials)
235
+
236
+ # Route based on payload content
237
+ cid = payload.get("cid")
238
+ if cid:
239
+ # Contact flow: JWT contains contact ID
240
+ return await get_contact_auth_middleware(request=request, token=token)
241
+
242
+ # User flow: Standard user authentication
243
+ return await get_user_auth_middleware(request=request, token=token)
244
+
51
245
  return get_auth
@@ -23,3 +23,12 @@ class GovCasbinRule(BaseCasbinRule):
23
23
  self.v3 = v3
24
24
  self.v4 = v4
25
25
  self.v5 = v5
26
+
27
+
28
+ def __str__(self):
29
+ arr = [self.ptype]
30
+ for v in (self.v0, self.v1, self.v2, self.v3):
31
+ if v is None:
32
+ break
33
+ arr.append(v)
34
+ return ", ".join(arr)
@@ -12,6 +12,7 @@ class Users(BaseModel):
12
12
  name = Column(String(100), nullable=False)
13
13
  is_active = Column(Boolean, default=True)
14
14
  last_login_at = Column(DateTime, nullable=True)
15
+ deleted_at = Column(DateTime, nullable=True) # Soft delete timestamp
15
16
 
16
17
  # Relationships
17
18
  roles = relationship(