abs-auth-rbac-core 0.3.2__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")
@@ -6,7 +6,8 @@ from typing import Callable, Any, Optional
6
6
 
7
7
  from .jwt_functions import JWTFunctions
8
8
  from .auth_functions import get_user_by_attribute
9
- from abs_exception_core.exceptions import UnauthorizedError, AuthError
9
+ from abs_exception_core.exceptions import UnauthorizedError, AuthError, NotFoundError
10
+ from abs_nosql_repository_core.repository import BaseRepository
10
11
  from fastapi.security.utils import get_authorization_scheme_param
11
12
 
12
13
  class CustomHTTPBearer(HTTPBearer):
@@ -77,4 +78,168 @@ def auth_middleware(
77
78
  logger.error(f"Authentication error: {str(e)}", exc_info=True)
78
79
  raise UnauthorizedError(detail="Authentication failed")
79
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
+
80
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(
@@ -4,7 +4,7 @@ from pydantic import BaseModel
4
4
  import casbin
5
5
  from casbin_sqlalchemy_adapter import Adapter
6
6
  from casbin_redis_watcher import RedisWatcher, WatcherOptions,new_watcher
7
- from sqlalchemy import and_, select
7
+ from sqlalchemy import and_, or_, select
8
8
  from sqlalchemy.orm import Session, joinedload
9
9
  from ..schema import CreatePermissionSchema
10
10
  from ..models import (
@@ -47,6 +47,43 @@ class RBACService:
47
47
  self.watcher = None
48
48
 
49
49
 
50
+ def _save_policy_if_watcher_active(self):
51
+ """
52
+ Helper method to save policy only if Redis watcher is active.
53
+ This ensures distributed systems stay in sync while avoiding
54
+ unnecessary load_policy calls on the current instance.
55
+ """
56
+ if self.is_watcher_active():
57
+ self.enforcer.save_policy()
58
+
59
+ def _bulk_operation_context(self):
60
+ """
61
+ Context manager for bulk Casbin operations.
62
+ Temporarily disables auto_save, executes operations, then saves once at the end.
63
+ This significantly reduces overhead for bulk policy changes.
64
+
65
+ Usage:
66
+ with self._bulk_operation_context():
67
+ self.enforcer.add_policies(policies)
68
+ """
69
+ class BulkOperationContext:
70
+ def __init__(self, service):
71
+ self.service = service
72
+ self.original_auto_save = None
73
+
74
+ def __enter__(self):
75
+ self.original_auto_save = self.service.enforcer.auto_save
76
+ self.service.enforcer.enable_auto_save(False)
77
+ return self
78
+
79
+ def __exit__(self, exc_type, exc_val, exc_tb):
80
+ if exc_type is None: # Only save if no exception occurred
81
+ self.service._save_policy_if_watcher_active()
82
+ self.service.enforcer.enable_auto_save(self.original_auto_save)
83
+ return False
84
+
85
+ return BulkOperationContext(self)
86
+
50
87
  def _initialize_casbin(self,redis_config:Optional[RedisWatcherSchema]=None):
51
88
  """
52
89
  Initiates the casbin policy using the default rules
@@ -65,8 +102,7 @@ class RBACService:
65
102
  self.enforcer = casbin.Enforcer(
66
103
  policy_path, adapter
67
104
  )
68
- # Load policies
69
- self.enforcer.load_policy()
105
+ self.enforcer.enable_auto_save(True)
70
106
 
71
107
  if redis_config:
72
108
  try:
@@ -109,8 +145,6 @@ class RBACService:
109
145
 
110
146
  self.enforcer.set_watcher(watcher)
111
147
  self.watcher = watcher
112
- self.enforcer.save_policy()
113
-
114
148
  except Exception as e:
115
149
  logger.error(f"Failed to initialize Redis watcher: {e}")
116
150
  self.watcher = None
@@ -119,31 +153,31 @@ class RBACService:
119
153
 
120
154
  def add_policy(self,role:str,resource:str,action:str,module:str):
121
155
  """
122
- Add a policy to the casbin enforcer
156
+ Add a policy to the casbin enforcer (optimized for distributed systems)
123
157
  """
124
- self.enforcer.add_policy(role,resource,action,module)
125
- self.enforcer.save_policy()
158
+ with self._bulk_operation_context():
159
+ self.enforcer.add_policy(role,resource,action,module)
126
160
 
127
161
  def remove_policy(self,role:str,resource:str,action:str,module:str):
128
162
  """
129
- Remove a policy from the casbin enforcer
163
+ Remove a policy from the casbin enforcer (optimized for distributed systems)
130
164
  """
131
- self.enforcer.remove_policy(role,resource,action,module)
132
- self.enforcer.save_policy()
165
+ with self._bulk_operation_context():
166
+ self.enforcer.remove_policy(role,resource,action,module)
133
167
 
134
168
  def add_policies(self,policies:List[Tuple[str,str,str,str]]):
135
169
  """
136
- Add a list of policies to the casbin enforcer
170
+ Add a list of policies to the casbin enforcer (optimized for distributed systems)
137
171
  """
138
- self.enforcer.add_policies(policies)
139
- self.enforcer.save_policy()
172
+ with self._bulk_operation_context():
173
+ self.enforcer.add_policies(policies)
140
174
 
141
175
  def remove_policies(self,policies:List[List[str]]):
142
176
  """
143
- Remove a list of policies from the casbin enforcer
177
+ Remove a list of policies from the casbin enforcer (optimized for distributed systems)
144
178
  """
145
- self.enforcer.remove_policies(policies)
146
- self.enforcer.save_policy()
179
+ with self._bulk_operation_context():
180
+ self.enforcer.remove_policies(policies)
147
181
 
148
182
  def enforce_policy(self,role:str,resource:str,action:str,module:str):
149
183
  """
@@ -153,13 +187,13 @@ class RBACService:
153
187
 
154
188
  def remove_filter_policy(self,index:int,value:str):
155
189
  """
156
- Remove a policy by filtering the policy
190
+ Remove a policy by filtering the policy (optimized for distributed systems)
157
191
  Args:
158
192
  index: The index of the policy to remove
159
193
  value: The value of the policy to remove
160
194
  """
161
- self.enforcer.remove_filtered_policy(index,value)
162
- self.enforcer.save_policy()
195
+ with self._bulk_operation_context():
196
+ self.enforcer.remove_filtered_policy(index,value)
163
197
 
164
198
  async def bulk_create_permissions(self,permissions:List[CreatePermissionSchema]):
165
199
  """
@@ -181,34 +215,64 @@ class RBACService:
181
215
  except Exception as e:
182
216
  raise e
183
217
 
184
- async def get_permissions_by_condition(self, condition: dict, use_filter_by: bool = True):
218
+ def build_filter(self,cond: dict):
219
+ if "and" in cond:
220
+ return and_(*[self.build_filter(c) for c in cond["and"]])
221
+ elif "or" in cond:
222
+ return or_(*[self.build_filter(c) for c in cond["or"]])
223
+ else:
224
+ # Multiple simple field=value pairs in the same dict
225
+ return and_(*[
226
+ getattr(Permission, field) == value
227
+ for field, value in cond.items()
228
+ ])
229
+
230
+
231
+ async def get_permissions_by_condition(self, condition: dict):
185
232
  """
186
- Get permission(s) based on a condition dict.
187
- If use_filter_by is True, assumes all conditions are `==`.
233
+ Get permission(s) based on nested logical conditions.
234
+
235
+ Example:
236
+ {
237
+ "and": [
238
+ {"entity_id": "123"},
239
+ {"or": [
240
+ {"user_id": "456"},
241
+ {"group_id": "789"}
242
+ ]}
243
+ ]
244
+ }
188
245
  """
189
246
  with self.db() as session:
190
247
  try:
191
- query = session.query(Permission)
192
- if use_filter_by:
193
- query = query.filter_by(**condition)
194
- else:
195
- filters = [getattr(Permission, k) == v for k, v in condition.items()]
196
- query = query.filter(*filters)
248
+ query = session.query(Permission).filter(self.build_filter(condition))
197
249
  return query.all()
198
250
  except Exception as e:
199
251
  raise e
200
-
252
+
201
253
  async def delete_permission_by_uuids(self,permission_uuids:List[str]):
202
254
  """
203
- Delete a permission by uuids
255
+ Delete permissions by uuids (optimized for distributed systems)
256
+ Handles cascade deletion in proper order:
257
+ 1. Delete UserPermission associations
258
+ 2. Delete RolePermission associations
259
+ 3. Delete Permissions
204
260
  """
205
261
  with self.db() as session:
206
262
  try:
263
+ # Step 1: Delete user_permissions associations
207
264
  user_permissions = session.query(UserPermission).filter(UserPermission.permission_uuid.in_(permission_uuids)).delete(synchronize_session=False)
265
+
266
+ # Step 2: Delete role_permissions associations (CRITICAL for FK constraint)
267
+ role_permissions = session.query(RolePermission).filter(RolePermission.permission_uuid.in_(permission_uuids)).delete(synchronize_session=False)
268
+
269
+ # Step 3: Delete permissions
208
270
  permissions = session.query(Permission).filter(Permission.uuid.in_(permission_uuids))
209
- for permission in permissions:
210
- self.enforcer.remove_filtered_policy(1,permission.resource)
211
- self.enforcer.save_policy()
271
+
272
+ with self._bulk_operation_context():
273
+ for permission in permissions:
274
+ self.enforcer.remove_filtered_policy(1,permission.resource)
275
+
212
276
  permissions.delete(synchronize_session=False)
213
277
  session.commit()
214
278
  return True
@@ -234,29 +298,38 @@ class RBACService:
234
298
  raise e
235
299
 
236
300
  def attach_permissions_to_user(self, user_uuid: str, permission_uuids: List[str]):
301
+ """
302
+ Attach permissions to user (optimized for distributed systems)
303
+ """
237
304
  with self.db() as session:
238
305
  try:
239
- user_permissions = [
240
- UserPermission(user_uuid=user_uuid, permission_uuid=permission_uuid)
306
+ # Use bulk_insert_mappings for better performance
307
+ user_permissions_data = [
308
+ {"user_uuid": user_uuid, "permission_uuid": permission_uuid}
241
309
  for permission_uuid in permission_uuids
242
310
  ]
243
- session.bulk_save_objects(user_permissions)
311
+ session.bulk_insert_mappings(UserPermission, user_permissions_data)
244
312
  session.commit()
245
313
 
314
+ # Fetch permissions and build policies
246
315
  permissions = session.query(Permission).filter(Permission.uuid.in_(permission_uuids)).all()
247
316
  policies = [
248
317
  [f"user:{user_uuid}", permission.resource, permission.action, permission.module]
249
318
  for permission in permissions
250
319
  ]
251
- added_policies = self.enforcer.add_policies(policies)
252
- if added_policies:
253
- self.enforcer.save_policy()
254
- self.enforcer.load_policy()
320
+
321
+ # Use context manager for optimized bulk operation
322
+ with self._bulk_operation_context():
323
+ self.enforcer.add_policies(policies)
324
+
255
325
  return self.get_user_only_permissions(user_uuid)
256
326
  except Exception as e:
257
327
  raise e
258
328
 
259
329
  def revoke_user_permissions(self, user_uuid: str, permission_uuids: List[str]):
330
+ """
331
+ Revoke permissions from user (optimized for distributed systems)
332
+ """
260
333
  with self.db() as session:
261
334
  try:
262
335
  user_permissions = session.query(UserPermission).filter(
@@ -269,16 +342,91 @@ class RBACService:
269
342
  [f"user:{user_uuid}", permission.resource, permission.action, permission.module]
270
343
  for permission in permissions
271
344
  ]
272
- removed_policies = self.enforcer.remove_policies(policies)
273
- if removed_policies:
274
- self.enforcer.save_policy()
275
- self.enforcer.load_policy()
345
+ with self._bulk_operation_context():
346
+ self.enforcer.remove_policies(policies)
347
+
276
348
  user_permissions.delete(synchronize_session=False)
277
349
  session.commit()
278
350
  return self.get_user_only_permissions(user_uuid)
279
351
  except Exception as e:
280
352
  raise e
281
-
353
+
354
+ def revoke_all_user_access(self, user_uuid: str) -> dict:
355
+ """
356
+ Revoke all roles and permissions from a user in a single operation.
357
+ This is typically used when soft-deleting a user.
358
+
359
+ Args:
360
+ user_uuid: The UUID of the user to revoke all access from
361
+
362
+ Returns:
363
+ dict: Summary of revoked roles and permissions
364
+ """
365
+ with self.db() as session:
366
+ try:
367
+ if not session.is_active:
368
+ session.begin()
369
+
370
+ # Get all user roles
371
+ user_roles = (
372
+ session.query(UserRole)
373
+ .options(joinedload(UserRole.role))
374
+ .filter(UserRole.user_uuid == user_uuid)
375
+ .all()
376
+ )
377
+
378
+ role_uuids = [ur.role.uuid for ur in user_roles] if user_roles else []
379
+
380
+ # Get all direct user permissions
381
+ user_permissions = (
382
+ session.query(UserPermission)
383
+ .options(joinedload(UserPermission.permission))
384
+ .filter(UserPermission.user_uuid == user_uuid)
385
+ .all()
386
+ )
387
+
388
+ permission_uuids = [up.permission.uuid for up in user_permissions] if user_permissions else []
389
+
390
+ # Revoke all roles
391
+ if role_uuids:
392
+ session.query(UserRole).filter(
393
+ UserRole.user_uuid == user_uuid,
394
+ UserRole.role_uuid.in_(role_uuids)
395
+ ).delete(synchronize_session=False)
396
+
397
+ # Revoke all direct permissions and remove from Casbin
398
+ if permission_uuids:
399
+ permissions = session.query(Permission).filter(
400
+ Permission.uuid.in_(permission_uuids)
401
+ ).all()
402
+
403
+ # Remove Casbin policies (optimized)
404
+ policies = [
405
+ [f"user:{user_uuid}", permission.resource, permission.action, permission.module]
406
+ for permission in permissions
407
+ ]
408
+ with self._bulk_operation_context():
409
+ self.enforcer.remove_policies(policies)
410
+
411
+ # Delete from database
412
+ session.query(UserPermission).filter(
413
+ UserPermission.user_uuid == user_uuid,
414
+ UserPermission.permission_uuid.in_(permission_uuids)
415
+ ).delete(synchronize_session=False)
416
+
417
+ session.commit()
418
+
419
+ return {
420
+ "user_uuid": user_uuid,
421
+ "roles_revoked": len(role_uuids),
422
+ "permissions_revoked": len(permission_uuids),
423
+ "role_uuids": role_uuids,
424
+ "permission_uuids": permission_uuids
425
+ }
426
+
427
+ except Exception as e:
428
+ raise e
429
+
282
430
  def list_roles(self) -> Any:
283
431
  """
284
432
  Get the list of all roles
@@ -362,13 +510,13 @@ class RBACService:
362
510
  ]
363
511
  session.bulk_insert_mappings(RolePermission, role_permissions)
364
512
 
365
- # Batch add Casbin policies
513
+ # Batch add Casbin policies (optimized)
366
514
  policies = [
367
515
  [role.uuid, permission.resource, permission.action, permission.module]
368
516
  for permission in existing_permissions
369
517
  ]
370
- self.enforcer.add_policies(policies)
371
- self.enforcer.save_policy()
518
+ with self._bulk_operation_context():
519
+ self.enforcer.add_policies(policies)
372
520
 
373
521
  # Commit transaction
374
522
  session.commit()
@@ -392,7 +540,7 @@ class RBACService:
392
540
  if not role:
393
541
  raise NotFoundError(detail="Requested role does not exist")
394
542
 
395
- return role
543
+ return role
396
544
 
397
545
  def update_role_permissions(
398
546
  self,
@@ -402,6 +550,7 @@ class RBACService:
402
550
  description: Optional[str] = None,
403
551
  ) -> Any:
404
552
  """Update role permissions by replacing all existing permissions with new ones"""
553
+
405
554
  with self.db() as session:
406
555
  try:
407
556
  if not session.is_active:
@@ -438,50 +587,45 @@ class RBACService:
438
587
  role.description = description
439
588
 
440
589
  if permissions is not None:
441
- existing_permissions = role.permissions
442
-
443
- # Remove Casbin policies for existing permissions
444
- remove_policies = [
445
- [role_uuid, existing_permission.resource, existing_permission.action, existing_permission.module]
446
- for existing_permission in existing_permissions
447
- ]
448
- self.enforcer.remove_policies(remove_policies)
449
- self.enforcer.save_policy()
450
-
451
- # Delete existing role permissions
452
- session.query(RolePermission).filter(
453
- RolePermission.role_uuid == role_uuid
454
- ).delete(synchronize_session=False)
455
-
456
- if permissions:
457
- # Fetch all permissions in a single query
458
- permissions_objs = (
459
- session.query(Permission)
460
- .filter(Permission.uuid.in_(permissions))
461
- .all()
462
- )
463
-
464
- found_permission_ids = {p.uuid for p in permissions_objs}
465
- missing_permission_ids = set(permissions) - found_permission_ids
466
- if missing_permission_ids:
467
- raise NotFoundError(
468
- detail=f"Permissions with UUIDs '{', '.join(missing_permission_ids)}' not found"
469
- )
470
-
471
- # Bulk insert role permissions
472
- role_permissions = [
473
- {"role_uuid": role_uuid, "permission_uuid": permission.uuid}
474
- for permission in permissions_objs
475
- ]
476
- session.bulk_insert_mappings(RolePermission, role_permissions)
477
-
478
- # Add Casbin policies
479
- policies = [
480
- [role_uuid, permission.resource, permission.action, permission.module]
481
- for permission in permissions_objs
482
- ]
483
- self.enforcer.add_policies(policies)
484
- self.enforcer.save_policy()
590
+ # Update permissions with optimized Casbin operations
591
+ with self._bulk_operation_context():
592
+ # Remove ALL existing policies for this role from Casbin
593
+ self.enforcer.remove_filtered_policy(0, str(role_uuid))
594
+
595
+ # Delete existing role permissions from database
596
+ session.query(RolePermission).filter(
597
+ RolePermission.role_uuid == role_uuid
598
+ ).delete(synchronize_session=False)
599
+
600
+ # Add new permissions if provided
601
+ if permissions:
602
+ # Fetch all permissions in a single query
603
+ permissions_objs = (
604
+ session.query(Permission)
605
+ .filter(Permission.uuid.in_(permissions))
606
+ .all()
607
+ )
608
+
609
+ found_permission_ids = {p.uuid for p in permissions_objs}
610
+ missing_permission_ids = set(permissions) - found_permission_ids
611
+ if missing_permission_ids:
612
+ raise NotFoundError(
613
+ detail=f"Permissions with UUIDs '{', '.join(missing_permission_ids)}' not found"
614
+ )
615
+
616
+ # Bulk insert role permissions
617
+ role_permissions = [
618
+ {"role_uuid": role_uuid, "permission_uuid": permission.uuid}
619
+ for permission in permissions_objs
620
+ ]
621
+ session.bulk_insert_mappings(RolePermission, role_permissions)
622
+
623
+ # Add new Casbin policies
624
+ policies = [
625
+ [role_uuid, permission.resource, permission.action, permission.module]
626
+ for permission in permissions_objs
627
+ ]
628
+ self.enforcer.add_policies(policies)
485
629
 
486
630
  session.commit()
487
631
 
@@ -519,12 +663,11 @@ class RBACService:
519
663
  [role.uuid, permission.resource, permission.action, permission.module]
520
664
  for permission in role.permissions
521
665
  ]
522
-
523
- # Remove all policies at once
524
- if remove_policies:
525
666
 
526
- self.enforcer.remove_policies(remove_policies)
527
- self.enforcer.save_policy()
667
+ # Remove all policies at once (optimized)
668
+ if remove_policies:
669
+ with self._bulk_operation_context():
670
+ self.enforcer.remove_policies(remove_policies)
528
671
 
529
672
  # Delete role (cascade will handle role_permissions and user_roles)
530
673
  session.delete(role)
@@ -645,13 +788,13 @@ class RBACService:
645
788
  )
646
789
  ).delete(synchronize_session=False)
647
790
 
648
- # Remove Casbin policies
791
+ # Remove Casbin policies (optimized)
649
792
  policies_to_remove = [
650
793
  [role.uuid, permission.resource, permission.action, permission.module]
651
794
  for permission in permissions_to_revoke
652
795
  ]
653
- self.enforcer.remove_policies(policies_to_remove)
654
- self.enforcer.save_policy()
796
+ with self._bulk_operation_context():
797
+ self.enforcer.remove_policies(policies_to_remove)
655
798
 
656
799
  session.commit()
657
800
 
@@ -713,13 +856,13 @@ class RBACService:
713
856
  ]
714
857
  session.bulk_insert_mappings(RolePermission, role_permissions)
715
858
 
716
- # Add Casbin policies
859
+ # Add Casbin policies (optimized)
717
860
  policies_to_add = [
718
861
  [role.uuid, permission.resource, permission.action, permission.module]
719
862
  for permission in new_permissions
720
863
  ]
721
- self.enforcer.add_policies(policies_to_add)
722
- self.enforcer.save_policy()
864
+ with self._bulk_operation_context():
865
+ self.enforcer.add_policies(policies_to_add)
723
866
 
724
867
  session.commit()
725
868
 
@@ -0,0 +1,4 @@
1
+ from .role_repository import RoleRepository
2
+ from .permission_repository import PermissionRepository
3
+
4
+ __all__ = ["RoleRepository", "PermissionRepository"]
@@ -0,0 +1,12 @@
1
+ from typing import Any, List, Optional, Callable
2
+ from sqlalchemy.orm import joinedload
3
+ from contextlib import AbstractContextManager
4
+ from sqlalchemy.orm import Session
5
+ from abs_repository_core.repository.base_repository import BaseRepository
6
+ from abs_repository_core.schemas.base_schema import FindBase, FindUniqueValues
7
+ from abs_auth_rbac_core.models.permissions import Permission
8
+
9
+ class PermissionRepository(BaseRepository):
10
+ def __init__(self, db: Callable[..., Session]):
11
+ self.db = db
12
+ super().__init__(db, Permission)
@@ -0,0 +1,18 @@
1
+ from typing import Any, List, Optional, Callable
2
+ from sqlalchemy.orm import joinedload
3
+ from contextlib import AbstractContextManager
4
+ from sqlalchemy.orm import Session
5
+ from abs_repository_core.repository.base_repository import BaseRepository
6
+ from abs_auth_rbac_core.models.roles import Role
7
+ from abs_exception_core.exceptions import NotFoundError
8
+ from abs_repository_core.schemas import FilterSchema, FindBase
9
+
10
+
11
+ class RoleRepository(BaseRepository):
12
+ def __init__(self, db: Callable[..., Session]):
13
+ self.db = db
14
+ super().__init__(db, Role)
15
+
16
+
17
+
18
+
@@ -0,0 +1,4 @@
1
+ from .role_service import RoleService
2
+ from .permission_service import PermissionService
3
+
4
+ __all__ = ["RoleService", "PermissionService"]
@@ -0,0 +1,15 @@
1
+ from typing import Any, List, Optional
2
+ from abs_repository_core.services.base_service import BaseService
3
+ from abs_repository_core.schemas.base_schema import FindBase, FindUniqueValues
4
+ from abs_auth_rbac_core.models.roles import Role
5
+ from abs_auth_rbac_core.repository.permission_repository import PermissionRepository
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class PermissionService(BaseService):
10
+ def __init__(self, repository:PermissionRepository):
11
+ super().__init__(repository)
12
+ self.repository = repository
13
+
14
+ def list_permissions(self, schema: FindBase, eager: bool = True):
15
+ return self.repository.read_by_options(schema, eager)
@@ -0,0 +1,18 @@
1
+ from typing import Any, List, Optional
2
+ from abs_repository_core.services.base_service import BaseService
3
+ from abs_repository_core.schemas.base_schema import FindBase, FindUniqueValues
4
+ from abs_auth_rbac_core.models.roles import Role
5
+ from abs_auth_rbac_core.repository.role_repository import RoleRepository
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class RoleService(BaseService):
10
+ def __init__(self, repository:RoleRepository):
11
+ self.repository = repository
12
+ super().__init__(repository)
13
+
14
+
15
+ def list_roles(self, schema: FindBase, eager: bool = True):
16
+ return self.repository.read_by_options(schema, eager)
17
+
18
+
@@ -153,6 +153,11 @@ class PermissionAction(str, Enum):
153
153
  VIEW_IMPORT_DATA = "VIEW_IMPORT_DATA"
154
154
  VIEW_WORKFLOW = "VIEW_WORKFLOW"
155
155
  VIEW_ERROR_DATA = "VIEW_ERROR_DATA"
156
+ VIEW_ENTITY_RECORD = "VIEW_ENTITY_RECORD"
157
+ EDIT_ENTITY_RECORD = "EDIT_ENTITY_RECORD"
158
+ DELETE_ENTITY_RECORD = "DELETE_ENTITY_RECORD"
159
+ CREATE_ENTITY_RECORD = "CREATE_ENTITY_RECORD"
160
+ CREATE_ENTITY_VIEW = "CREATE_ENTITY_VIEW"
156
161
 
157
162
 
158
163
 
@@ -172,12 +177,18 @@ class PermissionModule(str, Enum):
172
177
  ASL = "ASL"
173
178
  EDS = "EDS"
174
179
  WORKFORCE_AGENT = "WORKFORCE_AGENT"
175
-
180
+ ENTITY = "ENTITY"
181
+ ENTITY_VIEW = "ENTITY_VIEW"
182
+ ENTITY_RECORD = "ENTITY_RECORD"
183
+ CHATBOT = "CHATBOT"
184
+ APPS = "APPS"
176
185
 
177
186
 
178
187
  class PermissionResource(str, Enum):
188
+ ENTITIES = "ENTITIES"
179
189
  DASHBOARD = "DASHBOARDS"
180
190
  WORKFORCE_AGENT = "WORKFORCE_AGENT"
191
+ CHATBOT = "CHATBOT"
181
192
  EPAR = "EPAR"
182
193
  EPARS = "EPARS"
183
194
  OCFO = "OCFO"
@@ -262,6 +273,7 @@ class PermissionResource(str, Enum):
262
273
  AUTOMATION_ACTION = "AUTOMATION_ACTION"
263
274
  AUTOMATION_INPUT = "AUTOMATION_INPUT"
264
275
  AUTOMATION_HISTORY = "AUTOMATION_HISTORY"
276
+ USER_PROFILE = "USER_PROFILE"
265
277
 
266
278
 
267
279
  class PermissionData(NamedTuple):
@@ -273,6 +285,22 @@ class PermissionData(NamedTuple):
273
285
 
274
286
 
275
287
  class PermissionConstants:
288
+ # User Profile Permissions
289
+ USER_PROFILE_VIEW = PermissionData(
290
+ name="View User Profile",
291
+ description="Permission to view user profile",
292
+ module=PermissionModule.USER_MANAGEMENT,
293
+ resource=PermissionResource.USER_PROFILE,
294
+ action=PermissionAction.VIEW,
295
+ )
296
+ USER_PROFILE_EDIT = PermissionData(
297
+ name="Edit User Profile",
298
+ description="Permission to edit user profile",
299
+ module=PermissionModule.USER_MANAGEMENT,
300
+ resource=PermissionResource.USER_PROFILE,
301
+ action=PermissionAction.EDIT,
302
+ )
303
+
276
304
  # Automation Builder Permissions
277
305
  AUTOMATION_HISTORY_VIEW = PermissionData(
278
306
  name="View Automation History",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: abs-auth-rbac-core
3
- Version: 0.3.2
3
+ Version: 0.3.18
4
4
  Summary: RBAC and Auth core utilities including JWT token management.
5
5
  License: MIT
6
6
  Author: AutoBridgeSystems
@@ -11,12 +11,14 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
- Requires-Dist: abs-exception-core (>=0.2.0,<0.3.0)
15
- Requires-Dist: abs-utils (>=0.4.1,<0.5.0)
14
+ Requires-Dist: abs-exception-core (>=0.2.2,<0.3.0)
15
+ Requires-Dist: abs-nosql-repository-core (>=0.11.8,<0.12.0)
16
+ Requires-Dist: abs-repository-core (>=0.3.0,<0.4.0)
17
+ Requires-Dist: abs-utils (>=0.4.3,<0.5.0)
16
18
  Requires-Dist: casbin (>=1.41.0,<2.0.0)
17
19
  Requires-Dist: casbin-redis-watcher (>=1.3.0,<2.0.0)
18
20
  Requires-Dist: casbin-sqlalchemy-adapter (>=1.4.0,<2.0.0)
19
- Requires-Dist: fastapi[standard] (>=0.115.12,<0.116.0)
21
+ Requires-Dist: fastapi[standard] (>=0.115.2)
20
22
  Requires-Dist: passlib (>=1.7.4,<2.0.0)
21
23
  Requires-Dist: psycopg2-binary (>=2.9.10,<3.0.0)
22
24
  Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
@@ -1,8 +1,8 @@
1
1
  abs_auth_rbac_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- abs_auth_rbac_core/auth/__init__.py,sha256=Pvetd98VD9jsok1F9e82yS9kLZMFqTpbN51hkxZQHzw,67
3
- abs_auth_rbac_core/auth/auth_functions.py,sha256=fhfKRhtpE_J9MHu2jSsIA-cy77A3wCAQKbGGZeh4pe4,947
2
+ abs_auth_rbac_core/auth/__init__.py,sha256=5PUd5wjIVgdfoD8Zce2uOOE8TtBUhznVHPVwWaiw_9A,312
3
+ abs_auth_rbac_core/auth/auth_functions.py,sha256=p9sOExXOUDXHaP4dccraJ6Xv3RZu2dH4AtRv22yPKXU,1054
4
4
  abs_auth_rbac_core/auth/jwt_functions.py,sha256=9vhjWrxXdE8fVQ4FGrPj9y6PoSEsaeFohPhgI-3hToI,4111
5
- abs_auth_rbac_core/auth/middleware.py,sha256=sAHhPiSSiS1iqSl8-thsaZXRK4EQLFJ08BKzKPpgmmQ,2965
5
+ abs_auth_rbac_core/auth/middleware.py,sha256=09gpOdvfgV8QsFEQO46QIRgtTy9-0kPRCVob6RJeE1U,8897
6
6
  abs_auth_rbac_core/models/__init__.py,sha256=9ImboxQ04XxRjd5o1RDBn465BOj3F2pahuVXF15NuqE,292
7
7
  abs_auth_rbac_core/models/base_model.py,sha256=AaWObslm8sTetv4H1Ia_gPpi_75uF5z1o7Et9WAvstU,612
8
8
  abs_auth_rbac_core/models/gov_casbin_rule.py,sha256=9PpCQWg6TWeBvgjRcC2VxSSFMkNW-B9a4e2LmmDmmiY,1000
@@ -11,17 +11,23 @@ abs_auth_rbac_core/models/rbac_model.py,sha256=GbgMA-lJoU__xYP7wCw2FB9P1ftMzRAU2
11
11
  abs_auth_rbac_core/models/role_permission.py,sha256=KQ7MGwFvHhXgWL73TGH_elfG0rTLj5Ct11EWi6ypNb8,414
12
12
  abs_auth_rbac_core/models/roles.py,sha256=3g52YoCAnVdId4iaQa6Jz1NUTnaZTM_i_4oGF-FneA0,657
13
13
  abs_auth_rbac_core/models/seeder/permission_seeder.py,sha256=j-aUy8uLHnUWpMmw1DqXq8yJcqWxQo-D5QjY1S-ifyA,3652
14
- abs_auth_rbac_core/models/user.py,sha256=rLsK-Qy4AUbnfGUchr9hOPmdzaNcZ8whZpL9V4LU-AI,990
14
+ abs_auth_rbac_core/models/user.py,sha256=rlNXyQ22GKwrg4nIKhpyH-arDIgLOovZr0ZTQNjkLco,1064
15
15
  abs_auth_rbac_core/models/user_permission.py,sha256=t1_VUSFyyZAfJK71liDkFkg51yCshCbuRMG9rxFyOro,600
16
16
  abs_auth_rbac_core/models/user_role.py,sha256=20pqmtJPzlUrI9ulHGouk8XlFgrGG7I6ikctb8sMUGs,706
17
17
  abs_auth_rbac_core/rbac/__init__.py,sha256=oYjtpmfrkEbwWCBAWuRoU1fM4fCpBxkF_lwQrelK1As,79
18
18
  abs_auth_rbac_core/rbac/decorator.py,sha256=pEFAW0Nn2iE4KBctPhNOmO_VLeJFDX2V9v2LsCu6kHY,1824
19
19
  abs_auth_rbac_core/rbac/policy.conf,sha256=wghhhKxgZH0rPhh1QFrIpq9nevJT3s7OxxvXiU3zzuI,305
20
- abs_auth_rbac_core/rbac/service.py,sha256=A0Mg6HFUKzRo2pksOJGu-t_QztaB20ZECc5FfIQ8IDU,38447
20
+ abs_auth_rbac_core/rbac/service.py,sha256=rztdSP0wZ-uSv4ASzvxuoHoz3Yng2sYAXSHo-j_-18s,44315
21
+ abs_auth_rbac_core/repository/__init__.py,sha256=tuEdEV5HsePiaEg2Jrakf-QOR3evTeS-2Tq5VqbywyU,154
22
+ abs_auth_rbac_core/repository/permission_repository.py,sha256=SQJyyErrrMnTnLJjhwZythPbYVGt5z0N5GJ5fV6Gvuo,541
23
+ abs_auth_rbac_core/repository/role_repository.py,sha256=OEPpWIm_61rOljPEcejqXyOvowYDK8Uh5K_pvRLfb3Y,562
21
24
  abs_auth_rbac_core/schema/__init__.py,sha256=v9xibJ8Wr9k0u6PEYNK0LCGUJD71SB5vxu9BZG0S7tM,46
22
25
  abs_auth_rbac_core/schema/permission.py,sha256=XvxPU68FY0PFgkF4GR2bSrzNvFB8c8OgY_d0JOJvMc8,203
26
+ abs_auth_rbac_core/service/__init__.py,sha256=zzzxVCUYYb4heFksjbktWqbST3IcTcTfOMWWC-L5_A0,136
27
+ abs_auth_rbac_core/service/permission_service.py,sha256=tWasmKe0lr1QokmKzjD08O251_ppTnfN9amqVZX_CCU,661
28
+ abs_auth_rbac_core/service/role_service.py,sha256=Q68igKS-cArHaq-tqrjWPpptnrXYImRAEwKQep0ZOBQ,633
23
29
  abs_auth_rbac_core/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- abs_auth_rbac_core/util/permission_constants.py,sha256=EHM4ZkQmMWR-AyoSEf-pJL-EC_eZ4Q_JEp9w62GknHY,102747
25
- abs_auth_rbac_core-0.3.2.dist-info/METADATA,sha256=7q_MiRXEV0O829MFDvvqk0GMpa8ym0nvdGPASQ1_8Ys,23591
26
- abs_auth_rbac_core-0.3.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
- abs_auth_rbac_core-0.3.2.dist-info/RECORD,,
30
+ abs_auth_rbac_core/util/permission_constants.py,sha256=P7r6uW6f9YsuxSSsk_oRVN4YqD2CdYvfgnjEmID3czU,103776
31
+ abs_auth_rbac_core-0.3.18.dist-info/METADATA,sha256=5NYRBQI6vSXcu1c7KZl__Q2xgXNFQDyG5mJ7DbPFOzM,23694
32
+ abs_auth_rbac_core-0.3.18.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
33
+ abs_auth_rbac_core-0.3.18.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.1.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any