Skip to content

rbac

plantdb.commons.auth.rbac Link

Role-Based Access Control (RBAC) Manager

Provides a comprehensive framework for handling user permissions, roles, and group management within the PlantDB ecosystem. It supports fine‑grained access control for scans and user accounts, allowing administrators to create, modify, and revoke permissions with ease.

Key Features
  • Role and permission hierarchy with ADMIN, CONTRIBUTOR, and READER roles.
  • Group‑based sharing of scans and dynamic membership handling.
  • Decorator‑based permission checks (requires_permission) for method protection.
  • User account lifecycle operations: activation, deactivation, unlocking.
  • Full scan access control: ownership, group sharing, and global role fallback.
Usage Examples

from plantdb.commons.auth.rbac import RBACManager, Permission, User rbac = RBACManager()

Create a new user and assign a roleLink

user = rbac.users.create_user('alice', roles={'CONTRIBUTOR'})

Create a group and add the userLink

rbac.create_group(user, 'researchers') rbac.add_user_to_group(user, 'researchers', 'alice')

Check if the user can read a scanLink

scan_meta = {'owner': 'bob', 'sharing': ['researchers']} can_read = rbac.can_access_scan(user, scan_meta, Permission.READ) print(can_read) True

RBACManager Link

RBACManager(users_file='users.json', groups_file='groups.json', max_login_attempts=3, lockout_duration=900)

Manage Role-Based Access Control (RBAC) for users and permissions.

This class provides methods to determine which permissions a user has, check if a user has a specific permission, and verify if a user can access or perform operations on scans based on their roles and permissions.

Attributes:

Name Type Description
GUEST_USERNAME str

The username for the default guest user account.

groups GroupManager

Manager for handling group operations.

Examples:

>>> from plantdb.commons.auth.rbac import RBACManager
>>> from plantdb.commons.test_database import test_database
>>> db = test_database('all')
>>> db.connect()
>>> scan = db.get_scan('real_plant_analyzed')
>>> rbac = RBACManager()
>>> guest_user = rbac.create_guest_user('guest')
>>> can_read = rbac.can_access_scan(guest_user, scan.metadata, Permission.READ)

Initialize the RBACManager.

Parameters:

Name Type Description Default
users_file str

Path to the JSON file for storing users database. Default is 'users.json'.

'users.json'
groups_file str

Path to the JSON file for storing groups database. Defaults to "groups.json".

'groups.json'
max_login_attempts int

Maximum number of failed login attempts before account lockout. Defaults to 3.

3
lockout_duration int or timedelta

Account lockout duration in seconds. Defaults to 900 (15 minutes).

900
Source code in plantdb/commons/auth/rbac.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def __init__(self, users_file: str = 'users.json', groups_file: str = "groups.json", max_login_attempts=3,
             lockout_duration=900):
    """
    Initialize the RBACManager.

    Parameters
    ----------
    users_file : str, optional
        Path to the JSON file for storing users database. Default is 'users.json'.
    groups_file : str, optional
        Path to the JSON file for storing groups database. Defaults to "groups.json".
    max_login_attempts : int, optional
        Maximum number of failed login attempts before account lockout. Defaults to ``3``.
    lockout_duration : int or timedelta, optional
        Account lockout duration in seconds. Defaults to ``900`` (15 minutes).
    """
    self.logger = get_logger(__class__.__name__)
    self.users = UserManager(users_file, max_login_attempts, lockout_duration)
    self.groups = GroupManager(groups_file)

activate Link

activate(requesting_user, username)

Activate a user account - requires MANAGE_USERS permission

Source code in plantdb/commons/auth/rbac.py
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
@requires_permission(Permission.MANAGE_USERS)
def activate(self, requesting_user: User, username: str) -> bool:
    """Activate a user account - requires MANAGE_USERS permission"""
    if not self.users.exists(username):
        self.logger.warning(f"Attempt to activate non-existent user: {username}")
        return False

    user = self.users.get_user(username)
    if user.is_active:
        self.users.activate(user)
        self.logger.info(f"User {username} activated by {requesting_user.username}")
        return True
    else:
        self.logger.info(f"User {username} was already active")
    return True

add_user_to_group Link

add_user_to_group(user, group_name, username_to_add)

Add a user to a group if the requesting user has permission.

Parameters:

Name Type Description Default
user User

The user requesting to add a member.

required
group_name str

The name of the group.

required
username_to_add str

The username to add to the group.

required

Returns:

Type Description
bool

True if the user was added successfully, False if permission denied or operation failed.

Source code in plantdb/commons/auth/rbac.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
def add_user_to_group(self, user: User, group_name: str, username_to_add: str) -> bool:
    """
    Add a user to a group if the requesting user has permission.

    Parameters
    ----------
    user : User
        The user requesting to add a member.
    group_name : str
        The name of the group.
    username_to_add : str
        The username to add to the group.

    Returns
    -------
    bool
        True if the user was added successfully, False if permission denied or operation failed.
    """
    if not self.can_add_to_group(user, group_name):
        return False

    return self.groups.add_user_to_group(group_name, username_to_add)

can_access_scan Link

can_access_scan(user, scan_metadata, operation)

Check if a user can perform a specific operation on a scan dataset.

This method implements the complete access control logic including: - Owner-based access (owners get CONTRIBUTOR role for their scans) - Group-based access (shared group members get CONTRIBUTOR role) - Global role-based access (fallback to user's global role) - Admin override (admins can do everything)

Parameters:

Name Type Description Default
user User

The user requesting access.

required
scan_metadata dict

The scan metadata containing 'owner' and optional 'sharing' fields.

required
operation Permission

The operation/permission being requested.

required

Returns:

Type Description
bool

True if the user can perform the operation, False otherwise.

Source code in plantdb/commons/auth/rbac.py
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
def can_access_scan(self, user: User, scan_metadata: dict, operation: Permission) -> bool:
    """
    Check if a user can perform a specific operation on a scan dataset.

    This method implements the complete access control logic including:
    - Owner-based access (owners get CONTRIBUTOR role for their scans)
    - Group-based access (shared group members get CONTRIBUTOR role)
    - Global role-based access (fallback to user's global role)
    - Admin override (admins can do everything)

    Parameters
    ----------
    user : User
        The user requesting access.
    scan_metadata : dict
        The scan metadata containing 'owner' and optional 'sharing' fields.
    operation : Permission
        The operation/permission being requested.

    Returns
    -------
    bool
        ``True`` if the user can perform the operation, ``False`` otherwise.
    """
    scan_permissions = self.get_scan_permissions(user, scan_metadata)
    return operation in scan_permissions

can_access_scan_by_owner Link

can_access_scan_by_owner(user, scan_owner, operation)

Legacy method for checking scan access by owner name only.

This method provides backward compatibility but doesn't support group sharing. For full RBAC support, use can_access_scan() with full metadata.

Parameters:

Name Type Description Default
user User

The user requesting access.

required
scan_owner str

The username of the scan owner.

required
operation Permission

The operation/permission being requested.

required

Returns:

Type Description
bool

True if the user can perform the operation, False otherwise.

Source code in plantdb/commons/auth/rbac.py
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
def can_access_scan_by_owner(self, user: User, scan_owner: str, operation: Permission) -> bool:
    """
    Legacy method for checking scan access by owner name only.

    This method provides backward compatibility but doesn't support group sharing.
    For full RBAC support, use can_access_scan() with full metadata.

    Parameters
    ----------
    user : User
        The user requesting access.
    scan_owner : str
        The username of the scan owner.
    operation : Permission
        The operation/permission being requested.

    Returns
    -------
    bool
        True if the user can perform the operation, False otherwise.
    """
    # Create minimal metadata with just owner
    scan_metadata = {'owner': scan_owner}
    return self.can_access_scan(user, scan_metadata, operation)

can_add_to_group Link

can_add_to_group(user, group_name)

Check if a user can add members to a specific group.

Users can add members to a group if: 1. They are an admin (MANAGE_GROUPS permission), OR 2. They are a member of the group

Parameters:

Name Type Description Default
user User

The user requesting to add members.

required
group_name str

The name of the group.

required

Returns:

Type Description
bool

True if the user can add members to the group, False otherwise.

Source code in plantdb/commons/auth/rbac.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
def can_add_to_group(self, user: User, group_name: str) -> bool:
    """
    Check if a user can add members to a specific group.

    Users can add members to a group if:
    1. They are an admin (MANAGE_GROUPS permission), OR
    2. They are a member of the group

    Parameters
    ----------
    user : User
        The user requesting to add members.
    group_name : str
        The name of the group.

    Returns
    -------
    bool
        True if the user can add members to the group, False otherwise.
    """
    # Admins can manage any group
    if self.has_permission(user, Permission.MANAGE_GROUPS):
        return True

    # Group members can add users to their groups
    group = self.groups.get_group(group_name)
    return group is not None and group.has_user(user.username)

can_create_group Link

can_create_group(user)

Check if a user can create groups.

Any user with CONTRIBUTOR role or higher can create groups.

Parameters:

Name Type Description Default
user User

The user to check.

required

Returns:

Type Description
bool

True if the user can create groups, False otherwise.

Source code in plantdb/commons/auth/rbac.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def can_create_group(self, user: User) -> bool:
    """
    Check if a user can create groups.

    Any user with CONTRIBUTOR role or higher can create groups.

    Parameters
    ----------
    user : User
        The user to check.

    Returns
    -------
    bool
        True if the user can create groups, False otherwise.
    """
    return self.has_permission(user, Permission.CREATE)

can_delete_group Link

can_delete_group(user)

Check if a user can delete a group.

Only users with the MANAGE_GROUPS permission can delete groups.

Parameters:

Name Type Description Default
user User

The user requesting to delete the group.

required

Returns:

Type Description
bool

True if the user can delete the group, False otherwise.

Source code in plantdb/commons/auth/rbac.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
def can_delete_group(self, user: User) -> bool:
    """
    Check if a user can delete a group.

    Only users with the `MANAGE_GROUPS` permission can delete groups.

    Parameters
    ----------
    user : User
        The user requesting to delete the group.

    Returns
    -------
    bool
        ``True`` if the user can delete the group, ``False`` otherwise.
    """
    return self.has_permission(user, Permission.MANAGE_GROUPS)

can_manage_groups Link

can_manage_groups(user)

Check if a user can manage groups (create/delete groups).

Parameters:

Name Type Description Default
user User

The user to check.

required

Returns:

Type Description
bool

True if the user can manage groups, False otherwise.

Source code in plantdb/commons/auth/rbac.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def can_manage_groups(self, user: User) -> bool:
    """
    Check if a user can manage groups (create/delete groups).

    Parameters
    ----------
    user : User
        The user to check.

    Returns
    -------
    bool
        True if the user can manage groups, False otherwise.
    """
    return self.has_permission(user, Permission.MANAGE_GROUPS)

can_modify_scan_owner Link

can_modify_scan_owner(user, scan_metadata)

Check if a user can modify the 'owner' field of a scan.

Only admins can modify scan ownership.

Parameters:

Name Type Description Default
user User

The user requesting to modify ownership.

required
scan_metadata dict

The scan metadata.

required

Returns:

Type Description
bool

True if the user can modify the owner field, False otherwise.

Source code in plantdb/commons/auth/rbac.py
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
def can_modify_scan_owner(self, user: User, scan_metadata: dict) -> bool:
    """
    Check if a user can modify the 'owner' field of a scan.

    Only admins can modify scan ownership.

    Parameters
    ----------
    user : User
        The user requesting to modify ownership.
    scan_metadata : dict
        The scan metadata.

    Returns
    -------
    bool
        True if the user can modify the owner field, False otherwise.
    """
    return self.has_permission(user, Permission.MANAGE_USERS)

can_modify_scan_sharing Link

can_modify_scan_sharing(user, scan_metadata)

Check if a user can modify the 'sharing' field of a scan.

Users can modify sharing if they have WRITE permission for the scan (i.e., they are the owner, in a shared group, or have global CONTRIBUTOR+ role).

Parameters:

Name Type Description Default
user User

The user requesting to modify sharing.

required
scan_metadata dict

The scan metadata.

required

Returns:

Type Description
bool

True if the user can modify the sharing field, False otherwise.

Source code in plantdb/commons/auth/rbac.py
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
def can_modify_scan_sharing(self, user: User, scan_metadata: dict) -> bool:
    """
    Check if a user can modify the 'sharing' field of a scan.

    Users can modify sharing if they have WRITE permission for the scan
    (i.e., they are the owner, in a shared group, or have global CONTRIBUTOR+ role).

    Parameters
    ----------
    user : User
        The user requesting to modify sharing.
    scan_metadata : dict
        The scan metadata.

    Returns
    -------
    bool
        True if the user can modify the sharing field, False otherwise.
    """
    return self.can_access_scan(user, scan_metadata, Permission.WRITE)

create_group Link

create_group(user, name, users=None, description=None)

Create a new group if the user has permission.

Parameters:

Name Type Description Default
user User

The user creating the group.

required
name str

The unique name for the group.

required
users Optional[Set[str]]

Initial set of users to add to the group.

None
description Optional[str]

Optional description of the group.

None

Returns:

Type Description
Optional[Group]

The created group object if successful, None if permission denied.

Raises:

Type Description
ValueError

If a group with the same name already exists.

Source code in plantdb/commons/auth/rbac.py
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def create_group(self, user: User, name: str, users: Optional[Set[str]] = None,
                 description: Optional[str] = None) -> Optional[Group]:
    """
    Create a new group if the user has permission.

    Parameters
    ----------
    user : User
        The user creating the group.
    name : str
        The unique name for the group.
    users : Optional[Set[str]], optional
        Initial set of users to add to the group.
    description : Optional[str], optional
        Optional description of the group.

    Returns
    -------
    Optional[Group]
        The created group object if successful, None if permission denied.

    Raises
    ------
    ValueError
        If a group with the same name already exists.
    """
    if not self.can_create_group(user):
        return None

    return self.groups.create_group(name, user.username, users, description)

deactivate Link

deactivate(requesting_user, username)

Deactivate a user account - requires MANAGE_USERS permission

Source code in plantdb/commons/auth/rbac.py
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
@requires_permission(Permission.MANAGE_USERS)
def deactivate(self, requesting_user: User, username: str) -> bool:
    """Deactivate a user account - requires MANAGE_USERS permission"""
    if not self.users.exists(username):
        self.logger.warning(f"Attempt to deactivate non-existent user: {username}")
        return False

    user = self.users.get_user(username)
    if user.is_active:
        self.users.deactivate(user)
        self.logger.info(f"User {username} deactivated by {requesting_user.username}")
        return True
    else:
        self.logger.info(f"User {username} was already inactive")
    return True

delete_group Link

delete_group(user, group_name)

Delete a group if the user has permission.

Parameters:

Name Type Description Default
user User

The user requesting to delete the group.

required
group_name str

The name of the group to delete.

required

Returns:

Type Description
bool

True if the group was deleted successfully, False if permission denied or group not found.

Source code in plantdb/commons/auth/rbac.py
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
def delete_group(self, user: User, group_name: str) -> bool:
    """
    Delete a group if the user has permission.

    Parameters
    ----------
    user : User
        The user requesting to delete the group.
    group_name : str
        The name of the group to delete.

    Returns
    -------
    bool
        True if the group was deleted successfully, False if permission denied or group not found.
    """
    if not self.can_delete_group(user):
        return False

    return self.groups.delete_group(group_name)

ensure_scan_owner Link

ensure_scan_owner(scan_metadata)

Ensure a scan has an owner field, defaulting to guest if missing.

This method should be called when loading or creating scans to ensure the owner field is always present.

Parameters:

Name Type Description Default
scan_metadata dict

The scan metadata to check and potentially modify.

required

Returns:

Type Description
dict

The metadata with owner field guaranteed to be present.

Source code in plantdb/commons/auth/rbac.py
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
def ensure_scan_owner(self, scan_metadata: dict) -> dict:
    """
    Ensure a scan has an owner field, defaulting to guest if missing.

    This method should be called when loading or creating scans to ensure
    the owner field is always present.

    Parameters
    ----------
    scan_metadata : dict
        The scan metadata to check and potentially modify.

    Returns
    -------
    dict
        The metadata with owner field guaranteed to be present.
    """
    if 'owner' not in scan_metadata:
        scan_metadata = scan_metadata.copy()
        scan_metadata['owner'] = self.users.GUEST_USERNAME
    return scan_metadata

get_accessible_scans_for_user Link

get_accessible_scans_for_user(user, all_scan_metadata)

Filter scans to only include those the user has READ access to.

This method can be used to implement scan listing with proper access control.

Parameters:

Name Type Description Default
user User

The user requesting scan access.

required
all_scan_metadata Dict[str, dict]

Dictionary mapping scan IDs to their metadata.

required

Returns:

Type Description
Dict[str, dict]

Dictionary of scan IDs to metadata for scans the user can read.

Source code in plantdb/commons/auth/rbac.py
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
def get_accessible_scans_for_user(self, user: User, all_scan_metadata: Dict[str, dict]) -> Dict[str, dict]:
    """
    Filter scans to only include those the user has READ access to.

    This method can be used to implement scan listing with proper access control.

    Parameters
    ----------
    user : User
        The user requesting scan access.
    all_scan_metadata : Dict[str, dict]
        Dictionary mapping scan IDs to their metadata.

    Returns
    -------
    Dict[str, dict]
        Dictionary of scan IDs to metadata for scans the user can read.
    """
    accessible_scans = {}

    for scan_id, metadata in all_scan_metadata.items():
        # Ensure owner field is present
        metadata = self.ensure_scan_owner(metadata)

        # Check if user has READ access to this scan
        if self.can_access_scan(user, metadata, Permission.READ):
            accessible_scans[scan_id] = metadata

    return accessible_scans

get_effective_role_for_scan Link

get_effective_role_for_scan(user, scan_metadata)

Get the effective role a user has for a specific scan dataset.

This method determines the user's role based on: 1. Dataset ownership (owner gets CONTRIBUTOR role) 2. Group sharing (shared group members get CONTRIBUTOR role) 3. User's global role (fallback)

Parameters:

Name Type Description Default
user User

The user to check.

required
scan_metadata dict

The scan metadata containing 'owner' and optional 'sharing' fields.

required

Returns:

Type Description
Role

The effective role for this specific scan dataset.

Source code in plantdb/commons/auth/rbac.py
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
def get_effective_role_for_scan(self, user: User, scan_metadata: dict) -> Role:
    """
    Get the effective role a user has for a specific scan dataset.

    This method determines the user's role based on:
    1. Dataset ownership (owner gets CONTRIBUTOR role)
    2. Group sharing (shared group members get CONTRIBUTOR role)
    3. User's global role (fallback)

    Parameters
    ----------
    user : User
        The user to check.
    scan_metadata : dict
        The scan metadata containing 'owner' and optional 'sharing' fields.

    Returns
    -------
    Role
        The effective role for this specific scan dataset.
    """
    # Get the scan owner, default to guest if not specified
    scan_owner = scan_metadata.get('owner', self.users.GUEST_USERNAME)

    # If user is the owner, they get CONTRIBUTOR role (unless they're admin)
    if user.username == scan_owner:
        # Admins keep their admin privileges
        if Role.ADMIN in user.roles:
            return Role.ADMIN
        # Owners get at least CONTRIBUTOR privileges
        current_roles = user.roles
        if Role.ADMIN in current_roles:
            return Role.ADMIN
        elif Role.CONTRIBUTOR in current_roles or Role.READER in current_roles:
            return Role.CONTRIBUTOR
        else:
            # Fallback to CONTRIBUTOR for owners
            return Role.CONTRIBUTOR

    # Check if user belongs to any shared groups
    shared_groups = scan_metadata.get('sharing', [])
    if shared_groups:
        user_groups = self.groups.get_user_groups(user.username)
        user_group_names = {group.name for group in user_groups}

        # If user belongs to any shared group, they get CONTRIBUTOR role for this scan
        if any(group_name in user_group_names for group_name in shared_groups):
            # Admins keep their admin privileges
            if Role.ADMIN in user.roles:
                return Role.ADMIN
            # Shared group members get CONTRIBUTOR privileges for this scan
            return Role.CONTRIBUTOR

    # Fall back to user's highest global role
    if Role.ADMIN in user.roles:
        return Role.ADMIN
    elif Role.CONTRIBUTOR in user.roles:
        return Role.CONTRIBUTOR
    else:
        return Role.READER

get_scan_permissions Link

get_scan_permissions(user, scan_metadata)

Get the set of permissions a user has for a specific scan dataset.

This method considers the user's effective role for the specific scan, taking into account ownership and group sharing.

Parameters:

Name Type Description Default
user User

The user to check permissions for.

required
scan_metadata dict

The scan metadata containing 'owner' and optional 'sharing' fields.

required

Returns:

Type Description
Set[Permission]

The set of permissions the user has for this specific scan.

Source code in plantdb/commons/auth/rbac.py
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
def get_scan_permissions(self, user: User, scan_metadata: dict) -> Set[Permission]:
    """
    Get the set of permissions a user has for a specific scan dataset.

    This method considers the user's effective role for the specific scan,
    taking into account ownership and group sharing.

    Parameters
    ----------
    user : User
        The user to check permissions for.
    scan_metadata : dict
        The scan metadata containing 'owner' and optional 'sharing' fields.

    Returns
    -------
    Set[Permission]
        The set of permissions the user has for this specific scan.
    """
    effective_role = self.get_effective_role_for_scan(user, scan_metadata)
    return effective_role.permissions

get_user_groups Link

get_user_groups(username)

Get all groups that a user belongs to.

Parameters:

Name Type Description Default
username str

The username to search for.

required

Returns:

Type Description
List[Group]

A list of Group objects that the user belongs to.

Source code in plantdb/commons/auth/rbac.py
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def get_user_groups(self, username: str) -> List[Group]:
    """
    Get all groups that a user belongs to.

    Parameters
    ----------
    username : str
        The username to search for.

    Returns
    -------
    List[Group]
        A list of Group objects that the user belongs to.
    """
    return self.groups.get_user_groups(username)

get_user_permissions Link

get_user_permissions(user)

Get the set of permissions a user has based on their assigned roles.

This function returns a set containing all permissions directly assigned to the user as well as those inherited from any roles they are part of. The resulting set is a union of both direct and indirect permissions.

Parameters:

Name Type Description Default
user User

A User object representing the user whose permissions will be checked. This user must have attributes permissions and roles.

required

Returns:

Type Description
Set[Permission]

A set containing all permissions that the specified user has access to, including those inherited from roles.

Examples:

>>> from plantdb.commons.auth.models import Permission
>>> from plantdb.commons.auth.models import User
>>> role_admin = Role('admin')
>>> role_user = Role('user')
>>> permission_a = Permission()  # Mocking a specific permission
>>> permission_b = Permission()  # Mocking another specific permission
>>> user = User(permissions={permission_a}, roles={role_user})
>>> role_permissions = {Role('admin'): {permission_b}}
>>> user.get_user_permissions(user)
{<__main__.Permission object at 0x...>}
Notes

The result depends on the role_permissions attribute of the class instance. Ensure that this dictionary is properly initialized before calling this method.

See Also

User : Represents a user with permissions and roles. Permission : Represents a permission that can be assigned to users or roles. Role : Represents a role with specific permissions.

Source code in plantdb/commons/auth/rbac.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def get_user_permissions(self, user: User) -> Set[Permission]:
    """
    Get the set of permissions a user has based on their assigned roles.

    This function returns a set containing all permissions directly assigned to
    the user as well as those inherited from any roles they are part of. The
    resulting set is a union of both direct and indirect permissions.

    Parameters
    ----------
    user : User
        A `User` object representing the user whose permissions will be checked.
        This user must have attributes `permissions` and `roles`.

    Returns
    -------
    Set[Permission]
        A set containing all permissions that the specified user has access to,
        including those inherited from roles.

    Examples
    --------
    >>> from plantdb.commons.auth.models import Permission
    >>> from plantdb.commons.auth.models import User
    >>> role_admin = Role('admin')
    >>> role_user = Role('user')
    >>> permission_a = Permission()  # Mocking a specific permission
    >>> permission_b = Permission()  # Mocking another specific permission
    >>> user = User(permissions={permission_a}, roles={role_user})
    >>> role_permissions = {Role('admin'): {permission_b}}
    >>> user.get_user_permissions(user)  # doctest: +SKIP
    {<__main__.Permission object at 0x...>}

    Notes
    -----
    The result depends on the `role_permissions` attribute of the class instance.
    Ensure that this dictionary is properly initialized before calling this method.

    See Also
    --------
    User : Represents a user with permissions and roles.
    Permission : Represents a permission that can be assigned to users or roles.
    Role : Represents a role with specific permissions.

    """
    permissions = set(user.permissions) if user.permissions else set()
    for role in user.roles:
        permissions.update(role.permissions)
    return permissions

get_user_scan_role_summary Link

get_user_scan_role_summary(user, scan_metadata)

Get a summary of the user's access to a specific scan.

This is useful for debugging and user interfaces to show access levels.

Parameters:

Name Type Description Default
user User

The user to analyze.

required
scan_metadata dict

The scan metadata.

required

Returns:

Type Description
dict

A dictionary containing access information including: - effective_role: The user's effective role for this scan - permissions: List of permissions the user has - access_reason: Why the user has this level of access

Source code in plantdb/commons/auth/rbac.py
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
def get_user_scan_role_summary(self, user: User, scan_metadata: dict) -> dict:
    """
    Get a summary of the user's access to a specific scan.

    This is useful for debugging and user interfaces to show access levels.

    Parameters
    ----------
    user : User
        The user to analyze.
    scan_metadata : dict
        The scan metadata.

    Returns
    -------
    dict
        A dictionary containing access information including:
        - effective_role: The user's effective role for this scan
        - permissions: List of permissions the user has
        - access_reason: Why the user has this level of access
    """
    metadata = self.ensure_scan_owner(scan_metadata)
    effective_role = self.get_effective_role_for_scan(user, metadata)
    permissions = list(self.get_scan_permissions(user, metadata))

    # Determine access reason
    scan_owner = metadata.get('owner', self.users.GUEST_USERNAME)
    shared_groups = metadata.get('sharing', [])
    user_groups = {group.name for group in self.groups.get_user_groups(user.username)}

    access_reason = []

    if Role.ADMIN in user.roles:
        access_reason.append("admin_role")

    if user.username == scan_owner:
        access_reason.append("owner")

    shared_group_matches = [g for g in shared_groups if g in user_groups]
    if shared_group_matches:
        access_reason.append(f"group_member: {', '.join(shared_group_matches)}")

    if not access_reason:
        access_reason.append("global_role")

    return {
        'effective_role': effective_role.value,
        'permissions': [p.value for p in permissions],
        'access_reason': access_reason,
        'is_owner': user.username == scan_owner,
        'shared_groups': shared_group_matches if shared_groups else []
    }

has_permission Link

has_permission(user, permission)

Check if a user has a specific permission.

This function determines whether the given user has the specified permission, including administrative privileges.

Parameters:

Name Type Description Default
user User

The user object to check for permissions.

required
permission Permission

The permission level or type to verify against the user's permissions.

required

Returns:

Type Description
bool

True if the user has the given permission, False otherwise.

Notes

The function checks for the specific permission in the user's permission set.

See Also

get_user_permissions : Retrieve the list of permissions a user has.

Source code in plantdb/commons/auth/rbac.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def has_permission(self, user: User, permission: Permission) -> bool:
    """
    Check if a user has a specific permission.

    This function determines whether the given user has the specified permission,
    including administrative privileges.

    Parameters
    ----------
    user : User
        The user object to check for permissions.
    permission : Permission
        The permission level or type to verify against the user's permissions.

    Returns
    -------
    bool
        `True` if the user has the given permission, `False` otherwise.

    Notes
    -----
    The function checks for the specific permission in the user's permission set.

    See Also
    --------
    get_user_permissions : Retrieve the list of permissions a user has.
    """
    user_permissions = self.get_user_permissions(user)
    return permission in user_permissions

is_guest_user Link

is_guest_user(user)

Check if the given user is the guest user.

Parameters:

Name Type Description Default
user User

The user object to check.

required

Returns:

Type Description
bool

True if the user is the guest user, False otherwise.

Source code in plantdb/commons/auth/rbac.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def is_guest_user(self, user: User) -> bool:
    """
    Check if the given user is the guest user.

    Parameters
    ----------
    user : User
        The user object to check.

    Returns
    -------
    bool
        True if the user is the guest user, False otherwise.
    """
    return user.username == self.users.GUEST_USERNAME

list_groups Link

list_groups(user)

List all groups if the user has permission.

Parameters:

Name Type Description Default
user User

The user requesting the group list.

required

Returns:

Type Description
Optional[List[Group]]

A list of all groups if the user has permission, None otherwise.

Source code in plantdb/commons/auth/rbac.py
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
def list_groups(self, user: User) -> Optional[List[Group]]:
    """
    List all groups if the user has permission.

    Parameters
    ----------
    user : User
        The user requesting the group list.

    Returns
    -------
    Optional[List[Group]]
        A list of all groups if the user has permission, None otherwise.
    """
    # For now, any authenticated user can list groups
    # This can be restricted later if needed
    return self.groups.list_groups()

remove_user_from_group Link

remove_user_from_group(user, group_name, username_to_remove)

Remove a user from a group if the requesting user has permission.

Parameters:

Name Type Description Default
user User

The user requesting to remove a member.

required
group_name str

The name of the group.

required
username_to_remove str

The username to remove from the group.

required

Returns:

Type Description
bool

True if the user was removed successfully, False if permission denied or operation failed.

Source code in plantdb/commons/auth/rbac.py
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
def remove_user_from_group(self, user: User, group_name: str, username_to_remove: str) -> bool:
    """
    Remove a user from a group if the requesting user has permission.

    Parameters
    ----------
    user : User
        The user requesting to remove a member.
    group_name : str
        The name of the group.
    username_to_remove : str
        The username to remove from the group.

    Returns
    -------
    bool
        True if the user was removed successfully, False if permission denied or operation failed.
    """
    if not self.can_add_to_group(user, group_name):  # Same permission as adding
        return False

    return self.groups.remove_user_from_group(group_name, username_to_remove)

unlock Link

unlock(requesting_user, username)

Unlock a user account - requires MANAGE_USERS permission

Source code in plantdb/commons/auth/rbac.py
792
793
794
795
796
797
798
799
800
801
802
803
804
805
@requires_permission(Permission.MANAGE_USERS)
def unlock(self, requesting_user: User, username: str) -> bool:
    """Unlock a user account - requires MANAGE_USERS permission"""
    if not self.users.exists(username):
        self.logger.warning(f"Attempt to unlock non-existent user: {username}")
        return False

    user = self.users.get_user(username)
    if user.locked_until:
        self.users.unlock_user(user)
        self.logger.info(f"User {username} unlocked by {requesting_user.username}")
    else:
        self.logger.info(f"User {username} was not locked")
    return True

validate_scan_metadata_access Link

validate_scan_metadata_access(user, old_metadata, new_metadata)

Validate that a user can make the proposed metadata changes.

This method checks if the user has permission to modify specific fields like 'owner' and 'sharing' based on the RBAC rules.

Parameters:

Name Type Description Default
user User

The user attempting to modify metadata.

required
old_metadata dict

The current scan metadata.

required
new_metadata dict

The proposed new metadata.

required

Returns:

Type Description
bool

True if all proposed changes are allowed, False otherwise.

Source code in plantdb/commons/auth/rbac.py
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
def validate_scan_metadata_access(self, user: User, old_metadata: dict, new_metadata: dict) -> bool:
    """
    Validate that a user can make the proposed metadata changes.

    This method checks if the user has permission to modify specific fields
    like 'owner' and 'sharing' based on the RBAC rules.

    Parameters
    ----------
    user : User
        The user attempting to modify metadata.
    old_metadata : dict
        The current scan metadata.
    new_metadata : dict
        The proposed new metadata.

    Returns
    -------
    bool
        True if all proposed changes are allowed, False otherwise.
    """
    # Check if owner field is being modified
    old_owner = old_metadata.get('owner', self.users.GUEST_USERNAME)
    new_owner = new_metadata.get('owner', self.users.GUEST_USERNAME)

    if old_owner != new_owner:
        if not self.can_modify_scan_owner(user, old_metadata):
            return False

    # Check if sharing field is being modified
    old_sharing = old_metadata.get('sharing', [])
    new_sharing = new_metadata.get('sharing', [])

    if old_sharing != new_sharing:
        if not self.can_modify_scan_sharing(user, old_metadata):
            return False

    return True

validate_sharing_groups Link

validate_sharing_groups(sharing_groups)

Validate that all groups in the sharing list exist.

Parameters:

Name Type Description Default
sharing_groups List[str]

List of group names to validate.

required

Returns:

Type Description
bool

True if all groups exist, False otherwise.

Source code in plantdb/commons/auth/rbac.py
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
def validate_sharing_groups(self, sharing_groups: List[str]) -> bool:
    """
    Validate that all groups in the sharing list exist.

    Parameters
    ----------
    sharing_groups : List[str]
        List of group names to validate.

    Returns
    -------
    bool
        True if all groups exist, False otherwise.
    """
    for group_name in sharing_groups:
        if not self.groups.group_exists(group_name):
            return False
    return True

requires_permission Link

requires_permission(required_permissions)

Decorator to check if the specified user has the required permission(s).

Parameters:

Name Type Description Default
required_permission

A single permission string or list of permission that the user must have to access the decorated method.

required
Source code in plantdb/commons/auth/rbac.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def requires_permission(required_permissions: Union[Permission, List[Permission]]):
    """
    Decorator to check if the specified user has the required permission(s).

    Parameters
    ----------
    required_permission:  Union[Permission, List[Permission]]
        A single permission string or list of permission that the user must have to access the decorated method.
    """

    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(self, requesting_user: User, *args, **kwargs) -> Any:
            # Ensure required_permissions is always a list
            perms_to_check = (
                required_permissions if isinstance(required_permissions, list)
                else [required_permissions]
            )

            # Get user's permissions (assuming you are calling from a class who as a `_get_user_permissions` method)
            user_permissions = self.get_user_permissions(requesting_user)

            # Check if the requesting user has all the required permissions
            has_permission = all(perm in user_permissions for perm in perms_to_check)

            if not has_permission:
                perm_names = [perm.name if hasattr(perm, 'name') else str(perm) for perm in perms_to_check]
                raise PermissionError(
                    f"User '{requesting_user.username}' does not have required permission(s): {', '.join(perm_names)}"
                )

            return func(self, requesting_user, *args, **kwargs)

        return wrapper

    return decorator