diff --git a/neon_users_service/exceptions.py b/neon_users_service/exceptions.py index af6e472..8f660ea 100644 --- a/neon_users_service/exceptions.py +++ b/neon_users_service/exceptions.py @@ -8,4 +8,10 @@ class UserExistsError(Exception): class UserNotFoundError(Exception): """ Raised when trying to look up a user that does not exist. + """ + + +class ConfigurationError(KeyError): + """ + Raised when service configuration is not valid. """ \ No newline at end of file diff --git a/neon_users_service/service.py b/neon_users_service/service.py index fafaec2..c5568a9 100644 --- a/neon_users_service/service.py +++ b/neon_users_service/service.py @@ -1,12 +1,20 @@ +import hashlib +import re +from copy import copy from typing import Optional from ovos_config import Configuration from neon_users_service.databases import UserDatabase +from neon_users_service.exceptions import ConfigurationError +from neon_users_service.models import User class NeonUsersService: def __init__(self, config: Optional[dict] = None): self.config = config or Configuration().get("neon_users_service", {}) self.database = self.init_database() + if not self.database: + raise ConfigurationError(f"`{self.config.get('module')}` is not a " + f"valid database module.") def init_database(self) -> UserDatabase: module = self.config.get("module") @@ -16,3 +24,35 @@ def init_database(self) -> UserDatabase: return SQLiteUserDatabase(**module_config) # Other supported databases may be added here + @staticmethod + def _ensure_hashed(password: str) -> str: + """ + Generate the sha-256 hash for an input password to be stored in the + database. If the password is already a valid hash string, it will be + returned with no changes. + @param password: Input password string to be hashed + @retruns: A hexadecimal string representation of the sha-256 hash + """ + if re.compile(r"^[a-f0-9]{64}$").match(password): + password_hash = password + else: + password_hash = hashlib.sha256(password.encode("utf-8")).hexdigest() + return password_hash + + def create_user(self, user: User) -> User: + """ + Helper to create a new user. Includes a check that the input password + hash is valid, replacing string passwords with hashes as necessary. + @param user: The user to be created + @returns: The user as added to the database + """ + # Create a copy to prevent modifying the input object + user = copy(user) + user.password_hash = self._ensure_hashed(user.password_hash) + return self.database.create_user(user) + + def shutdown(self): + """ + Shutdown the service + """ + self.database.shutdown() diff --git a/tests/test_service.py b/tests/test_service.py new file mode 100644 index 0000000..2b21156 --- /dev/null +++ b/tests/test_service.py @@ -0,0 +1,50 @@ +import hashlib +import os +from unittest import TestCase +from os.path import join, dirname, isfile + +from neon_users_service.databases import UserDatabase +from neon_users_service.databases.sqlite import SQLiteUserDatabase +from neon_users_service.exceptions import ConfigurationError +from neon_users_service.models import User +from neon_users_service.service import NeonUsersService + + +class TestUsersService(TestCase): + test_db_path = join(dirname(__file__), 'test_db.sqlite') + test_config = {"module": "sqlite", + "sqlite": {"db_path": test_db_path}} + + def setUp(self): + if isfile(self.test_db_path): + os.remove(self.test_db_path) + + def test_create_service(self): + # Create with default config + service = NeonUsersService() + self.assertIsNotNone(service.config) + self.assertIsInstance(service.database, UserDatabase) + service.shutdown() + + # Create with valid passed configuration + service = NeonUsersService(self.test_config) + self.assertIsInstance(service.database, SQLiteUserDatabase) + self.assertTrue(isfile(self.test_db_path)) + service.shutdown() + + # Create with invalid configuration + with self.assertRaises(ConfigurationError): + NeonUsersService({"module": None}) + + def test_create_user(self): + service = NeonUsersService(self.test_config) + string_password = "super secret password" + hashed_password = hashlib.sha256(string_password.encode()).hexdigest() + user_1 = service.create_user(User(username="user_1", + password_hash=hashed_password)) + input_user_2 = User(username="user_2", password_hash=string_password) + user_2 = service.create_user(input_user_2) + self.assertEqual(user_1.password_hash, hashed_password) + self.assertEqual(user_2.password_hash, hashed_password) + # The input object should not be modified + self.assertNotEqual(user_2, input_user_2)