From a144c91113265c10656fa0e3f0922dce4f2d9ec6 Mon Sep 17 00:00:00 2001 From: dettlaff Date: Sat, 26 Oct 2024 22:22:31 +0400 Subject: [PATCH] refactor: moved json user management to a separate repository --- selfprivacy_api/actions/users.py | 84 ++++--------------- selfprivacy_api/graphql/common_types/user.py | 4 +- .../graphql/mutations/deprecated_mutations.py | 2 - .../graphql/mutations/users_mutations.py | 34 +++++--- .../repositories/users/__init__.py | 3 + .../users/abstract_user_repository.py | 47 +++++++++++ .../repositories/users/exceptions.py | 30 +++++++ .../users/json_user_repository.py | 38 +++++++++ ...users.py => test_users_json_repository.py} | 0 .../no_users.json | 0 .../one_user.json | 0 .../some_users.json | 0 .../undefined.json | 0 13 files changed, 159 insertions(+), 83 deletions(-) create mode 100644 selfprivacy_api/repositories/users/__init__.py create mode 100644 selfprivacy_api/repositories/users/abstract_user_repository.py create mode 100644 selfprivacy_api/repositories/users/exceptions.py create mode 100644 selfprivacy_api/repositories/users/json_user_repository.py rename tests/test_graphql/{test_users.py => test_users_json_repository.py} (100%) rename tests/test_graphql/{test_users => test_users_json_repository}/no_users.json (100%) rename tests/test_graphql/{test_users => test_users_json_repository}/one_user.json (100%) rename tests/test_graphql/{test_users => test_users_json_repository}/some_users.json (100%) rename tests/test_graphql/{test_users => test_users_json_repository}/undefined.json (100%) diff --git a/selfprivacy_api/actions/users.py b/selfprivacy_api/actions/users.py index 7dd83fa..f0e48a7 100644 --- a/selfprivacy_api/actions/users.py +++ b/selfprivacy_api/actions/users.py @@ -2,8 +2,6 @@ import re from typing import Optional -from pydantic import BaseModel -from enum import Enum from selfprivacy_api.utils import ( ReadUserData, WriteUserData, @@ -11,21 +9,21 @@ from selfprivacy_api.utils import ( is_username_forbidden, ) +from selfprivacy_api.repositories.users.abstract_user_repository import ( + UserDataUser, + UserDataUserOrigin, +) -class UserDataUserOrigin(Enum): - """Origin of the user in the user data""" - - NORMAL = "NORMAL" - PRIMARY = "PRIMARY" - ROOT = "ROOT" - - -class UserDataUser(BaseModel): - """The user model from the userdata file""" - - username: str - ssh_keys: list[str] - origin: UserDataUserOrigin +from selfprivacy_api.repositories.users.exceptions import ( + InvalidConfiguration, + PasswordIsEmpty, + UserAlreadyExists, + UserIsProtected, + UsernameForbidden, + UsernameNotAlphanumeric, + UsernameTooLong, + UserNotFound, +) def ensure_ssh_and_users_fields_exist(data): @@ -78,43 +76,7 @@ def get_users( return users -class UsernameForbidden(Exception): - """Attemted to create a user with a forbidden username""" - - pass - - -class UserAlreadyExists(Exception): - """Attemted to create a user that already exists""" - - pass - - -class UsernameNotAlphanumeric(Exception): - """Attemted to create a user with a non-alphanumeric username""" - - pass - - -class UsernameTooLong(Exception): - """Attemted to create a user with a too long username. Username must be less than 32 characters""" - - pass - - -class PasswordIsEmpty(Exception): - """Attemted to create a user with an empty password""" - - pass - - -class InvalidConfiguration(Exception): - """The userdata is broken""" - - pass - - -def create_user(username: str, password: str): +def create_user(username: str, password: str) -> None: if password == "": raise PasswordIsEmpty("Password is empty") @@ -150,19 +112,7 @@ def create_user(username: str, password: str): ) -class UserNotFound(Exception): - """Attemted to get a user that does not exist""" - - pass - - -class UserIsProtected(Exception): - """Attemted to delete a user that is protected""" - - pass - - -def delete_user(username: str): +def delete_user(username: str) -> None: with WriteUserData() as user_data: ensure_ssh_and_users_fields_exist(user_data) if username == user_data["username"] or username == "root": @@ -176,7 +126,7 @@ def delete_user(username: str): raise UserNotFound("User did not exist") -def update_user(username: str, password: str): +def update_user(username: str, password: str) -> None: if password == "": raise PasswordIsEmpty("Password is empty") diff --git a/selfprivacy_api/graphql/common_types/user.py b/selfprivacy_api/graphql/common_types/user.py index a515821..2dafd64 100644 --- a/selfprivacy_api/graphql/common_types/user.py +++ b/selfprivacy_api/graphql/common_types/user.py @@ -1,7 +1,7 @@ import typing from enum import Enum import strawberry -import selfprivacy_api.actions.users as users_actions +from selfprivacy_api.repositories.users import ACTIVE_USERS_PROVIDER as users_actions from selfprivacy_api.graphql.mutations.mutation_interface import ( MutationReturnInterface, @@ -31,7 +31,7 @@ class UserMutationReturn(MutationReturnInterface): def get_user_by_username(username: str) -> typing.Optional[User]: - user = users_actions.get_user_by_username(username) + user = users_actions.get_user_by_username(username=username) if user is None: return None diff --git a/selfprivacy_api/graphql/mutations/deprecated_mutations.py b/selfprivacy_api/graphql/mutations/deprecated_mutations.py index d9f3e3a..0e3345d 100644 --- a/selfprivacy_api/graphql/mutations/deprecated_mutations.py +++ b/selfprivacy_api/graphql/mutations/deprecated_mutations.py @@ -13,7 +13,6 @@ from selfprivacy_api.graphql.mutations.api_mutations import ( ApiMutations, DeviceApiTokenMutationReturn, ) -from selfprivacy_api.graphql.mutations.backup_mutations import BackupMutations from selfprivacy_api.graphql.mutations.job_mutations import JobMutations from selfprivacy_api.graphql.mutations.mutation_interface import ( GenericJobMutationReturn, @@ -30,7 +29,6 @@ from selfprivacy_api.graphql.mutations.system_mutations import ( SystemMutations, TimezoneMutationReturn, ) -from selfprivacy_api.graphql.mutations.backup_mutations import BackupMutations from selfprivacy_api.graphql.mutations.users_mutations import UsersMutations diff --git a/selfprivacy_api/graphql/mutations/users_mutations.py b/selfprivacy_api/graphql/mutations/users_mutations.py index 7644b90..37f4a84 100644 --- a/selfprivacy_api/graphql/mutations/users_mutations.py +++ b/selfprivacy_api/graphql/mutations/users_mutations.py @@ -2,8 +2,8 @@ """Users management module""" # pylint: disable=too-few-public-methods import strawberry + from selfprivacy_api.graphql import IsAuthenticated -from selfprivacy_api.actions.users import UserNotFound from selfprivacy_api.graphql.common_types.user import ( UserMutationReturn, get_user_by_username, @@ -18,7 +18,17 @@ from selfprivacy_api.actions.ssh import ( from selfprivacy_api.graphql.mutations.mutation_interface import ( GenericMutationReturn, ) -import selfprivacy_api.actions.users as users_actions +from selfprivacy_api.repositories.users import ACTIVE_USERS_PROVIDER as users_actions +from selfprivacy_api.repositories.users.exceptions import ( + PasswordIsEmpty, + UsernameForbidden, + InvalidConfiguration, + UserAlreadyExists, + UserIsProtected, + UsernameNotAlphanumeric, + UsernameTooLong, + UserNotFound, +) @strawberry.input @@ -45,37 +55,37 @@ class UsersMutations: def create_user(self, user: UserMutationInput) -> UserMutationReturn: try: users_actions.create_user(user.username, user.password) - except users_actions.PasswordIsEmpty as e: + except PasswordIsEmpty as e: return UserMutationReturn( success=False, message=str(e), code=400, ) - except users_actions.UsernameForbidden as e: + except UsernameForbidden as e: return UserMutationReturn( success=False, message=str(e), code=409, ) - except users_actions.UsernameNotAlphanumeric as e: + except UsernameNotAlphanumeric as e: return UserMutationReturn( success=False, message=str(e), code=400, ) - except users_actions.UsernameTooLong as e: + except UsernameTooLong as e: return UserMutationReturn( success=False, message=str(e), code=400, ) - except users_actions.InvalidConfiguration as e: + except InvalidConfiguration as e: return UserMutationReturn( success=False, message=str(e), code=400, ) - except users_actions.UserAlreadyExists as e: + except UserAlreadyExists as e: return UserMutationReturn( success=False, message=str(e), @@ -94,13 +104,13 @@ class UsersMutations: def delete_user(self, username: str) -> GenericMutationReturn: try: users_actions.delete_user(username) - except users_actions.UserNotFound as e: + except UserNotFound as e: return GenericMutationReturn( success=False, message=str(e), code=404, ) - except users_actions.UserIsProtected as e: + except UserIsProtected as e: return GenericMutationReturn( success=False, message=str(e), @@ -118,13 +128,13 @@ class UsersMutations: """Update user mutation""" try: users_actions.update_user(user.username, user.password) - except users_actions.PasswordIsEmpty as e: + except PasswordIsEmpty as e: return UserMutationReturn( success=False, message=str(e), code=400, ) - except users_actions.UserNotFound as e: + except UserNotFound as e: return UserMutationReturn( success=False, message=str(e), diff --git a/selfprivacy_api/repositories/users/__init__.py b/selfprivacy_api/repositories/users/__init__.py new file mode 100644 index 0000000..0fc536f --- /dev/null +++ b/selfprivacy_api/repositories/users/__init__.py @@ -0,0 +1,3 @@ +from selfprivacy_api.repositories.users.json_user_repository import JsonUserRepository + +ACTIVE_USERS_PROVIDER = JsonUserRepository diff --git a/selfprivacy_api/repositories/users/abstract_user_repository.py b/selfprivacy_api/repositories/users/abstract_user_repository.py new file mode 100644 index 0000000..dd53c77 --- /dev/null +++ b/selfprivacy_api/repositories/users/abstract_user_repository.py @@ -0,0 +1,47 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from pydantic import BaseModel +from enum import Enum + + +class UserDataUserOrigin(Enum): + """Origin of the user in the user data""" + + NORMAL = "NORMAL" + PRIMARY = "PRIMARY" + ROOT = "ROOT" + + +class UserDataUser(BaseModel): + """The user model from the userdata file""" + + username: str + ssh_keys: list[str] + origin: UserDataUserOrigin + + +class AbstractUserRepository(ABC): + + @abstractmethod + def get_users( + exclude_primary: bool = False, + exclude_root: bool = False, + ) -> list[UserDataUser]: + """Retrieves a list of users with options to exclude specific user groups""" + + @abstractmethod + def create_user(username: str, password: str): + """Creates a new user""" + + @abstractmethod + def delete_user(username: str) -> None: + """Deletes an existing user""" + + @abstractmethod + def update_user(username: str, password: str) -> None: + """Updates the password of an existing user""" + + @abstractmethod + def get_user_by_username(username: str) -> Optional[UserDataUser]: + """Retrieves user data (UserDataUser) by username""" diff --git a/selfprivacy_api/repositories/users/exceptions.py b/selfprivacy_api/repositories/users/exceptions.py new file mode 100644 index 0000000..9d176e1 --- /dev/null +++ b/selfprivacy_api/repositories/users/exceptions.py @@ -0,0 +1,30 @@ +class UserNotFound(Exception): + """Attemted to get a user that does not exist""" + + +class UserIsProtected(Exception): + """Attemted to delete a user that is protected""" + + +class UsernameForbidden(Exception): + """Attemted to create a user with a forbidden username""" + + +class UserAlreadyExists(Exception): + """Attemted to create a user that already exists""" + + +class UsernameNotAlphanumeric(Exception): + """Attemted to create a user with a non-alphanumeric username""" + + +class UsernameTooLong(Exception): + """Attemted to create a user with a too long username. Username must be less than 32 characters""" + + +class PasswordIsEmpty(Exception): + """Attemted to create a user with an empty password""" + + +class InvalidConfiguration(Exception): + """The userdata is broken""" diff --git a/selfprivacy_api/repositories/users/json_user_repository.py b/selfprivacy_api/repositories/users/json_user_repository.py new file mode 100644 index 0000000..25f2414 --- /dev/null +++ b/selfprivacy_api/repositories/users/json_user_repository.py @@ -0,0 +1,38 @@ +from typing import Optional + +from selfprivacy_api.repositories.users.abstract_user_repository import ( + AbstractUserRepository, + UserDataUser, +) + +from selfprivacy_api.actions.users import ( + create_user, + delete_user, + get_user_by_username, + get_users, + update_user, +) + + +class JsonUserRepository(AbstractUserRepository): + def get_users( + exclude_primary: bool = False, + exclude_root: bool = False, + ) -> list[UserDataUser]: + return get_users(exclude_primary=exclude_primary, exclude_root=exclude_root) + + def create_user(username: str, password: str): + """Creates a new user""" + return create_user(username=username, password=password) + + def delete_user(username: str) -> None: + """Deletes an existing user""" + return delete_user(username=username) + + def update_user(username: str, password: str) -> None: + """Updates the password of an existing user""" + return update_user(username=username, password=password) + + def get_user_by_username(username: str) -> Optional[UserDataUser]: + """Retrieves user data (UserDataUser) by username""" + return get_user_by_username(username=username) diff --git a/tests/test_graphql/test_users.py b/tests/test_graphql/test_users_json_repository.py similarity index 100% rename from tests/test_graphql/test_users.py rename to tests/test_graphql/test_users_json_repository.py diff --git a/tests/test_graphql/test_users/no_users.json b/tests/test_graphql/test_users_json_repository/no_users.json similarity index 100% rename from tests/test_graphql/test_users/no_users.json rename to tests/test_graphql/test_users_json_repository/no_users.json diff --git a/tests/test_graphql/test_users/one_user.json b/tests/test_graphql/test_users_json_repository/one_user.json similarity index 100% rename from tests/test_graphql/test_users/one_user.json rename to tests/test_graphql/test_users_json_repository/one_user.json diff --git a/tests/test_graphql/test_users/some_users.json b/tests/test_graphql/test_users_json_repository/some_users.json similarity index 100% rename from tests/test_graphql/test_users/some_users.json rename to tests/test_graphql/test_users_json_repository/some_users.json diff --git a/tests/test_graphql/test_users/undefined.json b/tests/test_graphql/test_users_json_repository/undefined.json similarity index 100% rename from tests/test_graphql/test_users/undefined.json rename to tests/test_graphql/test_users_json_repository/undefined.json