Skip to content

manager

plantdb.commons.auth.manager Link

User and Group Management Module

This module provides two primary classes – UserManager and GroupManager – to handle authentication, authorization, and role‑based access control in a lightweight file‑based persistence system. User data, including hashed passwords, roles, and lock‑out state, is stored in a JSON file, while group membership and metadata are kept in a separate JSON file. The module is designed for small projects or prototyping, offering straightforward APIs for creating users, managing passwords, locking out accounts, and organizing users into groups with associated permissions.

Key Features
  • Password hashing with Argon2 and secure verification.
  • Account lock‑out after configurable failed login attempts.
  • User activation/deactivation and role assignment.
  • Group management: create, delete, add/remove users, and query memberships.
  • Atomic JSON persistence to avoid data corruption.
  • Minimal dependencies – relies only on standard library plus argon2.
Usage Examples

from pathlib import Path from plantdb.commons.auth.manager import UserManager, GroupManager, Role

Initialize managers (will create JSON files if absent)Link

um = UserManager(users_file=Path('users.json')) gm = GroupManager(groups_file=Path('groups.json'))

Create a new userLink

um.create('alice', 'Alice Smith', 'secure123', roles={Role.ADMIN})

Validate credentialsLink

assert um.validate('alice', 'secure123')

Create a group and add the userLink

group = gm.create_group('admins', creator='alice', users={'alice'}, description='Admin group')

Check membershipLink

assert 'admins' in [g.name for g in gm.get_user_groups('alice')]

GroupManager Link

GroupManager(groups_file='groups.json')

Manages groups for the RBAC system.

This class handles the creation, modification, and persistence of user groups. Groups are stored in a JSON file and loaded/saved as needed.

Attributes:

Name Type Description
groups_file str

Path to the JSON file where groups are stored.

groups Dict[str, Group]

Dictionary mapping group names to Group objects.

Initialize the GroupManager.

Parameters:

Name Type Description Default
groups_file str

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

'groups.json'
Source code in plantdb/commons/auth/manager.py
628
629
630
631
632
633
634
635
636
637
638
639
640
def __init__(self, groups_file: str = "groups.json"):
    """
    Initialize the GroupManager.

    Parameters
    ----------
    groups_file : str, optional
        Path to the JSON file for storing groups. Defaults to "groups.json".
    """
    self.groups_file = Path(groups_file)
    self.groups: Dict[str, Group] = {}
    self.logger = get_logger(__class__.__name__)
    self._load_groups()

add_user_to_group Link

add_user_to_group(group_name, username)

Add a user to a group.

Parameters:

Name Type Description Default
group_name str

The name of the group.

required
username str

The username to add to the group.

required

Returns:

Type Description
bool

True if the user was added, False if the group doesn't exist or user was already in group.

Source code in plantdb/commons/auth/manager.py
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
def add_user_to_group(self, group_name: str, username: str) -> bool:
    """
    Add a user to a group.

    Parameters
    ----------
    group_name : str
        The name of the group.
    username : str
        The username to add to the group.

    Returns
    -------
    bool
        True if the user was added, False if the group doesn't exist or user was already in group.
    """
    group = self.get_group(group_name)
    if not group:
        return False

    result = group.add_user(username)
    if result:
        self._save_groups()
    return result

create_group Link

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

Create a new group.

Parameters:

Name Type Description Default
name str

The unique name for the group.

required
creator str

The username of the user creating the group.

required
users Optional[Set[str]]

Initial set of users to add to the group. Creator is automatically added.

None
description Optional[str]

Optional description of the group.

None

Returns:

Type Description
Group

The created group object.

Raises:

Type Description
ValueError

If a group with the same name already exists.

Source code in plantdb/commons/auth/manager.py
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
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
def create_group(self, name: str, creator: str, users: Optional[Set[str]] = None,
                 description: Optional[str] = None) -> Group:
    """
    Create a new group.

    Parameters
    ----------
    name : str
        The unique name for the group.
    creator : str
        The username of the user creating the group.
    users : Optional[Set[str]], optional
        Initial set of users to add to the group. Creator is automatically added.
    description : Optional[str], optional
        Optional description of the group.

    Returns
    -------
    Group
        The created group object.

    Raises
    ------
    ValueError
        If a group with the same name already exists.
    """
    if name in self.groups:
        raise ValueError(f"Group '{name}' already exists")

    # Initialize users set and ensure creator is included
    if users is None:
        users = set()
    users.add(creator)

    group = Group(
        name=name,
        users=users,
        description=description,
        created_at=datetime.now(),
        created_by=creator
    )

    self.groups[name] = group
    self._save_groups()
    return group

delete_group Link

delete_group(name)

Delete a group.

Parameters:

Name Type Description Default
name str

The name of the group to delete.

required

Returns:

Type Description
bool

True if the group was deleted, False if it didn't exist.

Source code in plantdb/commons/auth/manager.py
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
def delete_group(self, name: str) -> bool:
    """
    Delete a group.

    Parameters
    ----------
    name : str
        The name of the group to delete.

    Returns
    -------
    bool
        True if the group was deleted, False if it didn't exist.
    """
    if name not in self.groups:
        return False

    del self.groups[name]
    self._save_groups()
    return True

get_group Link

get_group(name)

Get a group by name.

Parameters:

Name Type Description Default
name str

The name of the group to retrieve.

required

Returns:

Type Description
Optional[Group]

The group object if it exists, None otherwise.

Source code in plantdb/commons/auth/manager.py
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
def get_group(self, name: str) -> Optional[Group]:
    """
    Get a group by name.

    Parameters
    ----------
    name : str
        The name of the group to retrieve.

    Returns
    -------
    Optional[Group]
        The group object if it exists, None otherwise.
    """
    return self.groups.get(name)

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/manager.py
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
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.
    """
    user_groups = []
    for group in self.groups.values():
        if group.has_user(username):
            user_groups.append(group)
    return user_groups

group_exists Link

group_exists(name)

Check if a group exists.

Parameters:

Name Type Description Default
name str

The name of the group to check.

required

Returns:

Type Description
bool

True if the group exists, False otherwise.

Source code in plantdb/commons/auth/manager.py
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
def group_exists(self, name: str) -> bool:
    """
    Check if a group exists.

    Parameters
    ----------
    name : str
        The name of the group to check.

    Returns
    -------
    bool
        True if the group exists, False otherwise.
    """
    return name in self.groups

list_groups Link

list_groups()

Get a list of all groups.

Returns:

Type Description
List[Group]

A list of all Group objects.

Source code in plantdb/commons/auth/manager.py
845
846
847
848
849
850
851
852
853
854
def list_groups(self) -> List[Group]:
    """
    Get a list of all groups.

    Returns
    -------
    List[Group]
        A list of all Group objects.
    """
    return list(self.groups.values())

remove_user_from_group Link

remove_user_from_group(group_name, username)

Remove a user from a group.

Parameters:

Name Type Description Default
group_name str

The name of the group.

required
username str

The username to remove from the group.

required

Returns:

Type Description
bool

True if the user was removed, False if the group doesn't exist or user wasn't in group.

Source code in plantdb/commons/auth/manager.py
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
def remove_user_from_group(self, group_name: str, username: str) -> bool:
    """
    Remove a user from a group.

    Parameters
    ----------
    group_name : str
        The name of the group.
    username : str
        The username to remove from the group.

    Returns
    -------
    bool
        True if the user was removed, False if the group doesn't exist or user wasn't in group.
    """
    group = self.get_group(group_name)
    if not group:
        return False

    result = group.remove_user(username)
    if result:
        self._save_groups()
    return result

UserManager Link

UserManager(users_file='users.json', max_login_attempts=3, lockout_duration=900)

UserManager class for managing user data.

The UserManager class provides methods to create, load, save, and manage users. It uses a JSON file to persist user data. It includes functionalities like password hashing and verification, user account lockout management, and handling of guest accounts.

Attributes:

Name Type Description
users_file str

Path to the JSON file where user data is stored.

users Dict[str, User]

Dictionary containing all users, indexed by username.

GUEST_USERNAME str

Default username for guest accounts.

GUEST_PASSWORD str

Default password for guest accounts.

Examples:

>>> from pathlib import Path
>>> from tempfile import gettempdir
>>> from plantdb.commons.auth.manager import UserManager
>>> manager = UserManager(users_file=Path(gettempdir())/'users.json')
>>> manager.validate_user_password(manager.GUEST_USERNAME, manager.GUEST_PASSWORD)
True
>>> manager.validate(manager.GUEST_USERNAME, manager.GUEST_PASSWORD)
True
>>> manager.validate('gest', manager.GUEST_PASSWORD)
False
>>> manager.deactivate(manager.get_user(manager.GUEST_USERNAME))
>>> manager.validate(manager.GUEST_USERNAME, manager.GUEST_PASSWORD)
WARNING  [UserManager] Account guest is not active.
False

User manager constructor.

Parameters:

Name Type Description Default
users_file str or Path

Path to the JSON file where user data is stored. Defaults to users.json.

'users.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/manager.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def __init__(self, users_file: str | Path = 'users.json', max_login_attempts=3, lockout_duration=900):
    """User manager constructor.

    Parameters
    ----------
    users_file : str or pathlib.Path, optional
        Path to the JSON file where user data is stored. Defaults to ``users.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.ADMIN_USERNAME = "admin"
    self.GUEST_USERNAME = "guest"
    self.GUEST_PASSWORD = "guest"
    self.users_file = Path(users_file)
    self.users: Dict[str, User] = {}
    self.logger = get_logger(__class__.__name__)
    self._load_users()
    self._ensure_guest_user()
    self._ensure_admin_user()

    self.max_login_attempts = max_login_attempts
    self.lockout_duration = timedelta(seconds=lockout_duration) if isinstance(lockout_duration,
                                                                              int) else lockout_duration

activate Link

activate(user)

Activates a user.

Parameters:

Name Type Description Default
user User

The user to activate.

required
Source code in plantdb/commons/auth/manager.py
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
def activate(self, user: User) -> None:
    """
    Activates a user.

    Parameters
    ----------
    user : plantdb.commons.auth.User
        The user to activate.
    """
    # Verify if the User exists
    try:
        assert self.exists(user.username)
    except AssertionError:
        self.logger.error(f"User '{user.username}' does not exists!")
        return

    user.is_active = True
    self._save_users()
    return

create Link

create(username, fullname, password, roles=None)

Create a new user and store the user information in a file.

Parameters:

Name Type Description Default
username str

The username of the user to create. This will be converted to lowercase.

required
fullname str

The full name of the user to create.

required
password str

The password of the user to create.

required
roles Role or Set[Role]

The roles to associate with the user. If None (default), use the Role.READER role.

None

Examples:

>>> from plantdb.commons.auth.manager import Role, UserManager
>>> manager = UserManager()
>>> manager.create('batman', "Bruce Wayne", "joker", Role.ADMIN)
>>> print(manager.users)
batman
Source code in plantdb/commons/auth/manager.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
def create(self, username: str, fullname: str, password: str, roles: Union[Role, Set[Role]] = None) -> None:
    """Create a new user and store the user information in a file.

    Parameters
    ----------
    username : str
        The username of the user to create.
        This will be converted to lowercase.
    fullname : str
        The full name of the user to create.
    password : str
        The password of the user to create.
    roles : Role or Set[Role], optional
        The roles to associate with the user.
        If ``None`` (default), use the `Role.READER` role.

    Examples
    --------
    >>> from plantdb.commons.auth.manager import Role, UserManager
    >>> manager = UserManager()
    >>> manager.create('batman', "Bruce Wayne", "joker", Role.ADMIN)
    >>> print(manager.users)
    batman
    """
    username = username.lower()  # Convert the username to lowercase to maintain uniformity.
    timestamp = datetime.now()  # Get the current timestamp for tracking user creation time.

    # Verify if the login is available
    try:
        assert not self.exists(username)
    except AssertionError:
        self.logger.error(f"User '{username}' already exists!")
        return

    # Convert to a list:
    if isinstance(roles, Role):
        roles = {roles}

    # Add the new user's data to the `self.users` dictionary.
    self.users[username] = User(
        username=username,
        fullname=fullname,
        password_hash=self._hash_password(password),
        roles=roles or {Role.READER},
        created_at=timestamp,
        password_last_change=timestamp,
    )
    # Save all user data (including the newly created user) to 'users.json' file.
    self._save_users()
    self.logger.debug(f"Created user '{username}' with fullname '{fullname}'.")
    if not username == self.GUEST_USERNAME:
        self.logger.info(f"Welcome {fullname}, please login...'")
    return

deactivate Link

deactivate(user)

Deactivates a user.

Parameters:

Name Type Description Default
user User

The user to deactivate.

required
Source code in plantdb/commons/auth/manager.py
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
def deactivate(self, user: User) -> None:
    """
    Deactivates a user.

    Parameters
    ----------
    user : plantdb.commons.auth.User
        The user to deactivate.
    """
    # Verify if the User exists
    try:
        assert self.exists(user.username)
    except AssertionError:
        self.logger.error(f"User '{user.username}' does not exists!")
        return

    user.is_active = False
    self._save_users()
    return

exists Link

exists(username)

Check if a user exists.

Parameters:

Name Type Description Default
username str

The name of the user to check for existence.

required

Returns:

Type Description
bool

True if the user exists, otherwise False.

Notes

This function is case-sensitive. For case-insensitive checks, convert both the username and the keys to lower or upper case before comparison.

Source code in plantdb/commons/auth/manager.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def exists(self, username: str) -> bool:
    """
    Check if a user exists.

    Parameters
    ----------
    username : str
        The name of the user to check for existence.

    Returns
    -------
    bool
        `True` if the user exists, otherwise `False`.

    Notes
    -----
    This function is case-sensitive. For case-insensitive checks, convert both
    the username and the keys to lower or upper case before comparison.
    """
    return username in self.users

get_user Link

get_user(username)

Retrieve a User object based on the provided username.

Parameters:

Name Type Description Default
username str

The unique identifier for the user to be retrieved.

required

Returns:

Type Description
Union[User, None]

An instance of the User class representing the requested user. If it does not exist, None is returned.

Source code in plantdb/commons/auth/manager.py
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def get_user(self, username: str) -> Union[User, None]:
    """
    Retrieve a User object based on the provided username.

    Parameters
    ----------
    username : str
        The unique identifier for the user to be retrieved.

    Returns
    -------
    Union[User, None]
        An instance of the User class representing the requested user.
        If it does not exist, `None` is returned.
    """
    if not self.exists(username):
        self.logger.error(f"User '{username}' does not exist!")
        return None
    return self.users[username]

is_active Link

is_active(username)

Check whether a user account is active.

Parameters:

Name Type Description Default
username str

The name of the user account to check for activity status.

required

Returns:

Type Description
bool

True if the user account is active, False otherwise.

Source code in plantdb/commons/auth/manager.py
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def is_active(self, username) -> bool:
    """
    Check whether a user account is active.

    Parameters
    ----------
    username : str
        The name of the user account to check for activity status.

    Returns
    -------
    bool
        ``True`` if the user account is active, ``False`` otherwise.
    """
    user = self.get_user(username)
    if not user.is_active:
        self.logger.warning(f"Account {username} is not active.")
    return user.is_active

is_locked_out Link

is_locked_out(username)

Check if an account is locked.

Parameters:

Name Type Description Default
username str

The username of the account to check.

required

Returns:

Type Description
bool

True if the account is locked, False otherwise.

Source code in plantdb/commons/auth/manager.py
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
def is_locked_out(self, username) -> bool:
    """
    Check if an account is locked.

    Parameters
    ----------
    username : str
        The username of the account to check.

    Returns
    -------
    bool
        ``True`` if the account is locked, ``False`` otherwise.
    """
    user = self.get_user(username)
    is_locked = user._is_locked_out()
    if is_locked:
        self.logger.info(f"Account {user} is locked, try logging in after {user.locked_until}.")
    return is_locked

unlock_user Link

unlock_user(user)

Unlock a specified user.

Parameters:

Name Type Description Default
username User

The user to unlock.

required
Source code in plantdb/commons/auth/manager.py
489
490
491
492
493
494
495
496
497
498
499
500
def unlock_user(self, user: User) -> None:
    """
    Unlock a specified user.

    Parameters
    ----------
    username : plantdb.commons.auth.User
        The user to unlock.
    """
    user.locked_until = None
    self._save_users()
    return

update_password Link

update_password(username, password, new_password)

Update the password of an existing user.

Parameters:

Name Type Description Default
username str

The name of the user whose password is to be updated.

required
password str

The current password of the user to verify their identity.

required
new_password str

The new password to set for the user. If None, no change will occur.

required
Source code in plantdb/commons/auth/manager.py
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
def update_password(self, username: str, password: str, new_password: str) -> None:
    """
    Update the password of an existing user.

    Parameters
    ----------
    username : str
        The name of the user whose password is to be updated.
    password : str
        The current password of the user to verify their identity.
    new_password : str
        The new password to set for the user. If None, no change will occur.
    """
    # Verify if the login exists
    try:
        assert self.exists(username)
    except AssertionError:
        self.logger.error(f"User '{username}' does not exists!")
        return

    if not self.validate(username, password):
        return

    user = self.get_user(username)
    timestamp = datetime.now()  # Get the current timestamp for tracking user creation time.
    if new_password:
        user.password = self._hash_password(new_password)
        user.password_last_change = timestamp

    self._save_users()
    self.logger.info(f"Password updated for user '{username}'...")
    return

validate Link

validate(username, password)

Validate the user credentials.

Parameters:

Name Type Description Default
username str

The username provided by the user attempting to log in.

required
password str

The password provided by the user attempting to log in.

required

Returns:

Type Description
bool

True if the login attempt is successful, False otherwise.

Source code in plantdb/commons/auth/manager.py
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
def validate(self, username: str, password: str) -> bool:
    """Validate the user credentials.

    Parameters
    ----------
    username : str
        The username provided by the user attempting to log in.
    password : str
        The password provided by the user attempting to log in.

    Returns
    -------
    bool
        ``True`` if the login attempt is successful, ``False`` otherwise.
    """
    if not self.exists(username):
        return False
    if not self.is_active(username):
        return False
    if self.is_locked_out(username):
        return False

    user = self.get_user(username)
    if self.validate_user_password(username, password):
        # Reset failed attempts on successful login
        user.failed_attempts = 0
        user.last_login = datetime.now()
        self._save_users()
        return True
    else:
        # Record a failed attemp
        self._record_failed_attempt(username, self.max_login_attempts, self.lockout_duration)
        self.logger.error(f"Invalid credentials for user '{username}'")
        return False

validate_user_password Link

validate_user_password(username, password)

Validate a user's password.

This function checks if the provided plaintext password matches the hashed password stored for the given username.

Parameters:

Name Type Description Default
username str

The username of the user.

required
password str

The plain-text password to validate.

required

Returns:

Type Description
bool

True if the password is valid, False otherwise.

Source code in plantdb/commons/auth/manager.py
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def validate_user_password(self, username: str, password: str) -> bool:
    """
    Validate a user's password.

    This function checks if the provided plaintext password matches the hashed password stored for the given username.

    Parameters
    ----------
    username : str
        The username of the user.
    password : str
        The plain-text password to validate.

    Returns
    -------
    bool
        ``True`` if the password is valid, ``False`` otherwise.

    """
    hash = self.get_user(username).password_hash
    try:
        # Verify password, raises exception if wrong.
        ph.verify(hash, password)
    except Exception as e:
        self.logger.error(f"Failed to verify password for {username}: {e}")
        return False
    else:
        return True