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.
- abs_auth_rbac_core/auth/__init__.py +13 -1
- abs_auth_rbac_core/auth/auth_functions.py +4 -1
- abs_auth_rbac_core/auth/middleware.py +197 -3
- abs_auth_rbac_core/models/gov_casbin_rule.py +9 -0
- abs_auth_rbac_core/models/user.py +1 -0
- abs_auth_rbac_core/rbac/service.py +425 -139
- abs_auth_rbac_core/repository/__init__.py +4 -0
- abs_auth_rbac_core/repository/permission_repository.py +12 -0
- abs_auth_rbac_core/repository/role_repository.py +18 -0
- abs_auth_rbac_core/service/__init__.py +4 -0
- abs_auth_rbac_core/service/permission_service.py +15 -0
- abs_auth_rbac_core/service/role_service.py +18 -0
- abs_auth_rbac_core/util/permission_constants.py +850 -16
- abs_auth_rbac_core-0.3.18.dist-info/METADATA +727 -0
- {abs_auth_rbac_core-0.1.8.dist-info → abs_auth_rbac_core-0.3.18.dist-info}/RECORD +16 -10
- abs_auth_rbac_core-0.1.8.dist-info/METADATA +0 -233
- {abs_auth_rbac_core-0.1.8.dist-info → abs_auth_rbac_core-0.3.18.dist-info}/WHEEL +0 -0
|
@@ -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__ = [
|
|
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(
|
|
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
|
-
|
|
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
|
|
@@ -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(
|