abs-auth-rbac-core 0.1.0__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.

Potentially problematic release.


This version of abs-auth-rbac-core might be problematic. Click here for more details.

@@ -0,0 +1,688 @@
1
+ from typing import List, Optional, Callable,Any
2
+ import os
3
+ import casbin
4
+ from casbin_sqlalchemy_adapter import Adapter
5
+ from sqlalchemy import and_, select
6
+ from sqlalchemy.orm import Session, joinedload
7
+
8
+ from ..models import (
9
+ Role,
10
+ RolePermission,
11
+ UserRole,
12
+ Users,
13
+ Permission
14
+ )
15
+
16
+ from abs_exception_core.exceptions import (
17
+ DuplicatedError,
18
+ NotFoundError,
19
+ PermissionDeniedError
20
+ )
21
+
22
+ from ..models.gov_casbin_rule import GovCasbinRule
23
+
24
+ class RBACService:
25
+ def __init__(self, session: Callable[...,Session]):
26
+ """
27
+ Service For Managing the RBAC
28
+ Args:
29
+ session: Callable[...,Session] -> Session of the SQLAlchemy database engine
30
+ """
31
+ self.db = session
32
+ self.enforcer = None
33
+ self._initialize_casbin()
34
+
35
+ def _initialize_casbin(self):
36
+ """
37
+ Initiates the casbin policy using the default rules
38
+ """
39
+ with self.db() as session:
40
+ engine = session.get_bind()
41
+
42
+ # Create the Casbin rule table if it doesn't exist
43
+ adapter = Adapter(engine,db_class=GovCasbinRule)
44
+
45
+ # Get the directory of the current file
46
+ current_dir = os.path.dirname(os.path.abspath(__file__))
47
+ # Construct the path to the policy file
48
+ policy_path = os.path.join(current_dir, "policy.conf")
49
+
50
+ self.enforcer = casbin.Enforcer(
51
+ policy_path, adapter
52
+ )
53
+ # Load policies
54
+ self.enforcer.load_policy()
55
+
56
+
57
+ def list_roles(self) -> Any:
58
+ """
59
+ Get the list of all roles
60
+ """
61
+ with self.db() as session:
62
+ """List all roles"""
63
+ total = session.query(Role).count()
64
+ roles = session.query(Role).all()
65
+ return {"roles": roles, "total": total}
66
+
67
+ def create_role(
68
+ self,
69
+ name: str,
70
+ description: Optional[str] = None,
71
+ permission_ids: List[str] = None,
72
+ ) -> Any:
73
+ """
74
+ Create role with the provided permissions
75
+
76
+ Args:
77
+ name: Name of the role
78
+ description: Optional description of the role
79
+ permission_ids: Optional list of permission UUIDs to assign to the role
80
+
81
+ Returns:
82
+ The created role object
83
+
84
+ Raises:
85
+ DuplicatedError: If a role with the same name already exists
86
+ NotFoundError: If any of the provided permission IDs don't exist
87
+ """
88
+ with self.db() as session:
89
+ try:
90
+ # Check if role with same name already exists
91
+ existing_role = session.query(Role).filter(Role.name == name).first()
92
+ if existing_role:
93
+ raise DuplicatedError(detail="Role already exists")
94
+
95
+ # Create the role
96
+ role = Role(name=name, description=description)
97
+ session.add(role)
98
+ session.flush() # Get the role UUID without committing
99
+
100
+ # If permission IDs are provided, assign them to the role
101
+ if permission_ids:
102
+ # Verify all permissions exist in a single query
103
+ permission_count = (
104
+ session.query(Permission)
105
+ .filter(Permission.uuid.in_(permission_ids))
106
+ .count()
107
+ )
108
+
109
+ # Check if all permissions were found
110
+ if permission_count != len(permission_ids):
111
+ # Find which permissions are missing
112
+ existing_permissions = (
113
+ session.query(Permission)
114
+ .filter(Permission.uuid.in_(permission_ids))
115
+ .all()
116
+ )
117
+ found_permission_ids = {p.uuid for p in existing_permissions}
118
+ missing_ids = set(permission_ids) - found_permission_ids
119
+ raise NotFoundError(
120
+ detail=f"Permissions with UUIDs '{', '.join(missing_ids)}' not found"
121
+ )
122
+
123
+ # Get all permissions for Casbin policy creation
124
+ existing_permissions = (
125
+ session.query(Permission)
126
+ .filter(Permission.uuid.in_(permission_ids))
127
+ .all()
128
+ )
129
+
130
+ # Bulk create role permissions using bulk_insert_mappings for better performance
131
+ role_permissions = [
132
+ {"role_uuid": role.uuid, "permission_uuid": permission_uuid}
133
+ for permission_uuid in permission_ids
134
+ ]
135
+ session.bulk_insert_mappings(RolePermission, role_permissions)
136
+
137
+ # Batch add Casbin policies
138
+ policies = [
139
+ (role.name, permission.resource, permission.action, permission.module)
140
+ for permission in existing_permissions
141
+ ]
142
+ self.enforcer.add_policies(policies)
143
+ self.enforcer.save_policy()
144
+
145
+ # Commit transaction
146
+ session.commit()
147
+ session.refresh(role)
148
+ return role
149
+
150
+ except Exception as e:
151
+ raise e
152
+
153
+ def get_role_with_permissions(self, role_uuid: str) -> Any:
154
+ """Get role details including its permissions"""
155
+ with self.db() as session:
156
+ # Use joinedload to eagerly load permissions
157
+ role = (
158
+ session.query(Role)
159
+ .options(joinedload(Role.permissions))
160
+ .filter(Role.uuid == role_uuid)
161
+ .first()
162
+ )
163
+
164
+ if not role:
165
+ raise NotFoundError(detail="Requested role does not exist")
166
+
167
+ return role
168
+
169
+ def update_role_permissions(
170
+ self,
171
+ role_uuid: str,
172
+ permissions: Optional[List[str]] = None,
173
+ name: Optional[str] = None,
174
+ description: Optional[str] = None,
175
+ ) -> Any:
176
+ """Update role permissions by replacing all existing permissions with new ones"""
177
+ with self.db() as session:
178
+ try:
179
+ if not session.is_active:
180
+ session.begin()
181
+
182
+ # Get role with eager loading of permissions
183
+ role = (
184
+ session.query(Role)
185
+ .options(joinedload(Role.permissions))
186
+ .filter(Role.uuid == role_uuid)
187
+ .first()
188
+ )
189
+
190
+ if not role:
191
+ raise NotFoundError(detail="Requested role does not exist")
192
+
193
+ # Update role information if provided
194
+ if name is not None or description is not None:
195
+ if name:
196
+ # Check if new name already exists for a different role
197
+ existing_role = (
198
+ session.query(Role)
199
+ .filter(Role.name == name, Role.uuid != role_uuid)
200
+ .first()
201
+ )
202
+
203
+ if existing_role:
204
+ raise DuplicatedError(detail="Role already exists")
205
+
206
+ if role.name != "super_admin":
207
+ role.name = name
208
+
209
+ if description is not None:
210
+ role.description = description
211
+
212
+ if permissions is not None:
213
+ existing_permissions = role.permissions
214
+
215
+ # Remove Casbin policies for existing permissions
216
+ for existing_permission in existing_permissions:
217
+ self.enforcer.remove_policy(
218
+ role.name,
219
+ existing_permission.resource,
220
+ existing_permission.action,
221
+ existing_permission.module
222
+ )
223
+ self.enforcer.save_policy()
224
+
225
+ # Delete existing role permissions
226
+ session.query(RolePermission).filter(
227
+ RolePermission.role_uuid == role_uuid
228
+ ).delete(synchronize_session=False)
229
+
230
+ if permissions:
231
+ # Fetch all permissions in a single query
232
+ permissions_objs = (
233
+ session.query(Permission)
234
+ .filter(Permission.uuid.in_(permissions))
235
+ .all()
236
+ )
237
+
238
+ found_permission_ids = {p.uuid for p in permissions_objs}
239
+ missing_permission_ids = set(permissions) - found_permission_ids
240
+ if missing_permission_ids:
241
+ raise NotFoundError(
242
+ detail=f"Permissions with UUIDs '{', '.join(missing_permission_ids)}' not found"
243
+ )
244
+
245
+ # Bulk insert role permissions
246
+ role_permissions = [
247
+ {"role_uuid": role_uuid, "permission_uuid": permission.uuid}
248
+ for permission in permissions_objs
249
+ ]
250
+ session.bulk_insert_mappings(RolePermission, role_permissions)
251
+
252
+ # Add Casbin policies
253
+ for permission in permissions_objs:
254
+ self.enforcer.add_policy(
255
+ role.name, permission.resource, permission.action, permission.module
256
+ )
257
+
258
+ self.enforcer.save_policy()
259
+
260
+ session.commit()
261
+
262
+ # Refresh the role to get the updated permissions
263
+ session.refresh(role)
264
+
265
+ # Return the updated role with permissions
266
+ return role
267
+
268
+ except Exception as e:
269
+ raise e
270
+
271
+ def delete_role(self, role_uuid: str,exception_roles:List[str]=None):
272
+ """Delete a role and its associated permissions"""
273
+ with self.db() as session:
274
+ try:
275
+ if not session.is_active:
276
+ session.begin()
277
+
278
+ role = self.get_role(role_uuid,session)
279
+
280
+ if exception_roles and len(exception_roles) > 0 and role.name in exception_roles:
281
+ raise PermissionDeniedError(detail="You are not allowed to delete the requested role.")
282
+
283
+ # Get role name for Casbin policy removal
284
+ role_name = role.name
285
+
286
+ # Delete role permissions
287
+ role_permissions = (
288
+ session.query(RolePermission)
289
+ .filter(RolePermission.role_uuid == role_uuid)
290
+ .all()
291
+ )
292
+
293
+ # Remove Casbin policies for each permission
294
+ remove_policies =[]
295
+ for role_permission in role_permissions:
296
+ permission = (
297
+ session.query(Permission)
298
+ .filter(Permission.uuid == role_permission.permission_uuid)
299
+ .first()
300
+ )
301
+ if permission:
302
+ remove_policies.append(
303
+ (role_name, permission.resource, permission.action, permission.module)
304
+ )
305
+
306
+ self.enforcer.remove_policies(remove_policies)
307
+ self.enforcer.save_policy()
308
+
309
+ # Delete role permissions
310
+ session.query(RolePermission).filter(
311
+ RolePermission.role_uuid == role_uuid
312
+ ).delete()
313
+
314
+ # Delete user role assignments
315
+ session.query(UserRole).filter(UserRole.role_uuid == role_uuid).delete()
316
+
317
+ # Delete role
318
+ session.delete(role)
319
+ session.commit()
320
+
321
+ except Exception as e:
322
+ raise e
323
+
324
+ def list_permissions(self) -> List[Any]:
325
+ """Get all permissions with their resources and actions"""
326
+ with self.db() as session:
327
+ return session.query(Permission).all()
328
+
329
+ def list_module_permissions(self,module:str) -> List[Any]:
330
+ """Get all permissions for a module"""
331
+ with self.db() as session:
332
+ return session.query(Permission).filter(Permission.module == module).all()
333
+
334
+ def get_user_permissions(self, user_uuid: str) -> List[Any]:
335
+ """Get all allowed permissions for a user"""
336
+ with self.db() as session:
337
+ # Get user roles with eager loading of roles and their permissions
338
+ user_roles = (
339
+ session.query(UserRole)
340
+ .join(Role, UserRole.role_uuid == Role.uuid)
341
+ .options(
342
+ joinedload(UserRole.role).joinedload(Role.permissions)
343
+ )
344
+ .filter(UserRole.user_uuid == user_uuid)
345
+ .all()
346
+ )
347
+
348
+ if not user_roles:
349
+ return []
350
+
351
+ # Build response directly from the eagerly loaded data
352
+ result = []
353
+ for user_role in user_roles:
354
+ role = user_role.role
355
+ for permission in role.permissions:
356
+ result.append(
357
+ {
358
+ "permission_id": permission.uuid,
359
+ "created_at": permission.created_at,
360
+ "role_id": role.uuid,
361
+ "updated_at": permission.updated_at,
362
+ "role_name": role.name,
363
+ "name": permission.name,
364
+ "resource": permission.resource,
365
+ "action": permission.action,
366
+ }
367
+ )
368
+
369
+ return result
370
+
371
+ def bulk_revoke_permissions(
372
+ self, role_uuid: str, permission_uuids: List[str]
373
+ ) -> Any:
374
+ """Revoke multiple permissions from a role"""
375
+ with self.db() as session:
376
+ try:
377
+ if not session.is_active:
378
+ session.begin()
379
+
380
+ # Get role with eager loading of permissions
381
+ role = (
382
+ session.query(Role)
383
+ .options(joinedload(Role.permissions))
384
+ .filter(Role.uuid == role_uuid)
385
+ .first()
386
+ )
387
+
388
+ if not role:
389
+ raise NotFoundError(detail="Requested role does not exist")
390
+
391
+ # Filter permissions to revoke from the eagerly loaded permissions
392
+ permissions_to_revoke = [
393
+ p for p in role.permissions
394
+ if p.uuid in permission_uuids
395
+ ]
396
+
397
+ if not permissions_to_revoke:
398
+ return role
399
+
400
+ # Get UUIDs of permissions to revoke
401
+ permission_uuids_to_revoke = [p.uuid for p in permissions_to_revoke]
402
+
403
+ # Delete role permissions
404
+ session.query(RolePermission).filter(
405
+ and_(
406
+ RolePermission.role_uuid == role_uuid,
407
+ RolePermission.permission_uuid.in_(permission_uuids_to_revoke),
408
+ )
409
+ ).delete(synchronize_session=False)
410
+
411
+ # Remove Casbin policies
412
+ policies_to_remove = [
413
+ (role.name, permission.resource, permission.action, permission.module)
414
+ for permission in permissions_to_revoke
415
+ ]
416
+ self.enforcer.remove_policies(policies_to_remove)
417
+ self.enforcer.save_policy()
418
+
419
+ session.commit()
420
+
421
+ # Refresh the role to get the updated permissions
422
+ session.refresh(role)
423
+ return role
424
+
425
+ except Exception as e:
426
+ raise e
427
+
428
+ def bulk_attach_permissions(
429
+ self, role_uuid: str, permission_uuids: List[str]
430
+ ) -> Any:
431
+ """Attach multiple permissions to a role"""
432
+ with self.db() as session:
433
+ try:
434
+ if not session.is_active:
435
+ session.begin()
436
+
437
+ # Get role with eager loading of permissions
438
+ role = (
439
+ session.query(Role)
440
+ .options(joinedload(Role.permissions))
441
+ .filter(Role.uuid == role_uuid)
442
+ .first()
443
+ )
444
+
445
+ if not role:
446
+ raise NotFoundError(detail="Requested role does not exist")
447
+
448
+ # Get existing permission UUIDs from the eagerly loaded permissions
449
+ existing_permission_uuids = {p.uuid for p in role.permissions}
450
+
451
+ # Calculate new permission UUIDs to attach
452
+ new_permission_uuids = set(permission_uuids) - existing_permission_uuids
453
+
454
+ if not new_permission_uuids:
455
+ return role
456
+
457
+ # Fetch new permissions in a single query
458
+ new_permissions = (
459
+ session.query(Permission)
460
+ .filter(Permission.uuid.in_(new_permission_uuids))
461
+ .all()
462
+ )
463
+
464
+ # Verify all permissions were found
465
+ if len(new_permissions) != len(new_permission_uuids):
466
+ found_permission_uuids = {p.uuid for p in new_permissions}
467
+ missing_permission_uuids = new_permission_uuids - found_permission_uuids
468
+ raise NotFoundError(
469
+ detail=f"Permissions with UUIDs '{', '.join(missing_permission_uuids)}' not found"
470
+ )
471
+
472
+ # Bulk insert role permissions
473
+ role_permissions = [
474
+ {"role_uuid": role_uuid, "permission_uuid": p.uuid}
475
+ for p in new_permissions
476
+ ]
477
+ session.bulk_insert_mappings(RolePermission, role_permissions)
478
+
479
+ # Add Casbin policies
480
+ policies_to_add = [
481
+ (role.name, permission.resource, permission.action, permission.module)
482
+ for permission in new_permissions
483
+ ]
484
+ self.enforcer.add_policies(policies_to_add)
485
+ self.enforcer.save_policy()
486
+
487
+ session.commit()
488
+
489
+ # Refresh the role to get the updated permissions
490
+ session.refresh(role)
491
+ return role
492
+
493
+ except Exception as e:
494
+ raise e
495
+
496
+ def get_user_roles(self, user_uuid: str,session: Optional[Session] = None) -> List[Any]:
497
+ """Get user roles"""
498
+ def query_roles(session: Session) -> List[Any]:
499
+ return (
500
+ session.query(Role)
501
+ .join(
502
+ UserRole,
503
+ and_(
504
+ UserRole.role_uuid == Role.uuid,
505
+ UserRole.user_uuid == user_uuid
506
+ )
507
+ )
508
+ .options(joinedload(Role.permissions))
509
+ .all()
510
+ )
511
+
512
+ if session:
513
+ return query_roles(session)
514
+ else:
515
+ with self.db() as new_session:
516
+ return query_roles(new_session)
517
+
518
+
519
+ def bulk_assign_roles_to_user(
520
+ self, user_uuid: str, role_uuids: List[str]
521
+ ) -> List[Any]:
522
+ """Assign multiple roles to a user"""
523
+ with self.db() as session:
524
+ try:
525
+ if not session.is_active:
526
+ session.begin()
527
+
528
+ current_roles = (
529
+ session.query(UserRole)
530
+ .options(joinedload(UserRole.role))
531
+ .filter(UserRole.user_uuid == user_uuid)
532
+ .all()
533
+ )
534
+
535
+ current_role_uuids = {role.role.uuid for role in current_roles}
536
+
537
+ new_role_uuids = set(role_uuids) - current_role_uuids
538
+
539
+ roles_to_remove = current_role_uuids - set(role_uuids)
540
+
541
+ if roles_to_remove:
542
+ session.query(UserRole).filter(
543
+ and_(
544
+ UserRole.user_uuid == user_uuid,
545
+ UserRole.role_uuid.in_(roles_to_remove),
546
+ )
547
+ ).delete(synchronize_session=False)
548
+
549
+ if new_role_uuids:
550
+ new_roles = (
551
+ session.query(Role).filter(Role.uuid.in_(new_role_uuids)).all()
552
+ )
553
+
554
+ if len(new_roles) != len(new_role_uuids):
555
+ raise NotFoundError(detail="One or more roles not found")
556
+
557
+ user_roles = [
558
+ UserRole(user_uuid=user_uuid, role_uuid=role.uuid)
559
+ for role in new_roles
560
+ ]
561
+ session.bulk_save_objects(user_roles)
562
+
563
+ session.commit()
564
+
565
+ return self.get_user_roles(user_uuid,session)
566
+
567
+ except Exception as e:
568
+ raise e
569
+
570
+ # Bulk Revoke Roles From User
571
+ def bulk_revoke_roles_from_user(
572
+ self, user_uuid: str, role_uuids: List[str]
573
+ ) -> List[Any]:
574
+ """Revoke multiple roles from a user"""
575
+ with self.db() as session:
576
+ try:
577
+ if not session.is_active:
578
+ session.begin()
579
+
580
+ current_roles = (
581
+ session.query(UserRole)
582
+ .options(joinedload(UserRole.role))
583
+ .filter(UserRole.user_uuid == user_uuid)
584
+ .filter(UserRole.role_uuid.in_(role_uuids))
585
+ .all()
586
+ )
587
+
588
+ if not current_roles:
589
+ return self.get_user_roles(user_uuid)
590
+
591
+ role_uuids_to_revoke = {role.role.uuid for role in current_roles}
592
+
593
+ session.query(UserRole).filter(
594
+ and_(
595
+ UserRole.user_uuid == user_uuid,
596
+ UserRole.role_uuid.in_(role_uuids_to_revoke),
597
+ )
598
+ ).delete(synchronize_session=False)
599
+
600
+ session.commit()
601
+
602
+ return self.get_user_roles(user_uuid,session)
603
+
604
+ except Exception as e:
605
+ raise e
606
+
607
+ def bulk_attach_roles_to_user(
608
+ self, user_uuid: str, role_uuids: List[str]
609
+ ) -> List[Any]:
610
+ """Attach multiple roles to a user"""
611
+ with self.db() as session:
612
+ try:
613
+ if not session.is_active:
614
+ session.begin()
615
+
616
+ current_roles = (
617
+ session.query(UserRole)
618
+ .options(joinedload(UserRole.role))
619
+ .filter(UserRole.user_uuid == user_uuid)
620
+ .all()
621
+ )
622
+
623
+ current_role_uuids = {role.role.uuid for role in current_roles}
624
+
625
+ new_role_uuids = set(role_uuids) - current_role_uuids
626
+
627
+ if not new_role_uuids:
628
+ return self.get_user_roles(user_uuid)
629
+
630
+ new_roles = (
631
+ session.query(Role).filter(Role.uuid.in_(new_role_uuids)).all()
632
+ )
633
+
634
+ if len(new_roles) != len(new_role_uuids):
635
+ raise NotFoundError(detail="There are some roles that does not exist.")
636
+
637
+ user_roles = [
638
+ UserRole(user_uuid=user_uuid, role_uuid=role.uuid) for role in new_roles
639
+ ]
640
+ session.bulk_save_objects(user_roles)
641
+
642
+ session.commit()
643
+
644
+ return self.get_user_roles(user_uuid,session)
645
+
646
+ except Exception as e:
647
+ raise e
648
+
649
+ def check_permission(self, user_uuid: str, resource: str, action: str, module: str) -> bool:
650
+ with self.db() as session:
651
+ roles = (
652
+ session.query(Role)
653
+ .join(
654
+ UserRole,
655
+ and_(
656
+ UserRole.role_uuid == Role.uuid,
657
+ UserRole.user_uuid == user_uuid,
658
+ ),
659
+ )
660
+ .all()
661
+ )
662
+ for role in roles:
663
+ # Try with module first
664
+ if self.enforcer.enforce(role.name, resource, action, module):
665
+ return True
666
+ return False
667
+
668
+ def check_permission_by_role(
669
+ self, role_name: str, resource: str, action: str, module: str
670
+ ) -> bool:
671
+ # Try with module first
672
+ if self.enforcer.enforce(role_name, resource, action, module):
673
+ return True
674
+ return False
675
+
676
+ def get_role(self, role_uuid: str,session: Optional[Session] = None) -> Any:
677
+ """Get role by uuid"""
678
+ def query_role(session: Session) -> Any:
679
+ role = session.query(Role).filter(Role.uuid == role_uuid).first()
680
+ if not role:
681
+ raise NotFoundError(detail="Requested role does not exist.")
682
+ return role
683
+
684
+ if session:
685
+ return query_role(session)
686
+ else:
687
+ with self.db() as session:
688
+ return query_role(session)
File without changes