From 18de70154e2f4828449f8bdf5f92f597cae487cf Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Sun, 5 Sep 2021 15:40:03 +0300 Subject: [PATCH] add option to set user-password for service when updating its hash --- src/ahriman/application/ahriman.py | 1 + .../application/handlers/create_user.py | 17 +++++---- src/ahriman/models/user.py | 5 +-- .../handlers/test_handler_create_user.py | 38 ++++++++++++++----- tests/ahriman/core/auth/test_mapping_auth.py | 2 +- tests/ahriman/models/test_user.py | 2 +- tests/ahriman/web/conftest.py | 2 +- 7 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index bd9d3fc2..75480161 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -154,6 +154,7 @@ def _set_create_user_parser(root: SubParserAction) -> argparse.ArgumentParser: description="create user for web services with password and role. In case if password was not entered it will be asked interactively", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("username", help="username for web service") + parser.add_argument("--as-service", help="add user as service user", action="store_true") parser.add_argument("-r", "--role", help="user role", type=UserAccess, choices=UserAccess, default=UserAccess.Read) parser.add_argument("-p", "--password", help="user password") parser.set_defaults( diff --git a/src/ahriman/application/handlers/create_user.py b/src/ahriman/application/handlers/create_user.py index b0834651..6fc9aca6 100644 --- a/src/ahriman/application/handlers/create_user.py +++ b/src/ahriman/application/handlers/create_user.py @@ -42,21 +42,26 @@ class CreateUser(Handler): :param configuration: configuration instance """ salt = CreateUser.get_salt(configuration) - user = CreateUser.create_user(args, salt) + user = CreateUser.create_user(args) auth_configuration = CreateUser.get_auth_configuration(configuration.include) - CreateUser.create_configuration(auth_configuration, user, salt) + CreateUser.create_configuration(auth_configuration, user, salt, args.as_service) @staticmethod - def create_configuration(configuration: Configuration, user: User, salt: str) -> None: + def create_configuration(configuration: Configuration, user: User, salt: str, as_service_user: bool) -> None: """ put new user to configuration :param configuration: configuration instance :param user: user descriptor :param salt: password hash salt + :param as_service_user: add user as service user, also set password and user to configuration """ section = Configuration.section_name("auth", user.access.value) configuration.set_option("auth", "salt", salt) - configuration.set_option(section, user.username, user.password) + configuration.set_option(section, user.username, user.hash_password(salt)) + + if as_service_user: + configuration.set_option("web", "username", user.username) + configuration.set_option("web", "password", user.password) if configuration.path is None: return @@ -64,17 +69,15 @@ class CreateUser(Handler): configuration.write(ahriman_configuration) @staticmethod - def create_user(args: argparse.Namespace, salt: str) -> User: + def create_user(args: argparse.Namespace) -> User: """ create user descriptor from arguments :param args: command line args - :param salt: password hash salt :return: built user descriptor """ user = User(args.username, args.password, args.role) if user.password is None: user.password = getpass.getpass() - user.password = user.hash_password(user.password, salt) return user @staticmethod diff --git a/src/ahriman/models/user.py b/src/ahriman/models/user.py index b76d9dc6..73e1f3b9 100644 --- a/src/ahriman/models/user.py +++ b/src/ahriman/models/user.py @@ -73,14 +73,13 @@ class User: verified: bool = self._HASHER.verify(password + salt, self.password) return verified - def hash_password(self, password: str, salt: str) -> str: + def hash_password(self, salt: str) -> str: """ generate hashed password from plain text - :param password: entered password :param salt: salt for hashed password :return: hashed string to store in configuration """ - password_hash: str = self._HASHER.hash(password + salt) + password_hash: str = self._HASHER.hash(self.password + salt) return password_hash def verify_access(self, required: UserAccess) -> bool: diff --git a/tests/ahriman/application/handlers/test_handler_create_user.py b/tests/ahriman/application/handlers/test_handler_create_user.py index 059024c9..f251857d 100644 --- a/tests/ahriman/application/handlers/test_handler_create_user.py +++ b/tests/ahriman/application/handlers/test_handler_create_user.py @@ -20,6 +20,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: args.username = "user" args.password = "pa55w0rd" args.role = UserAccess.Status + args.as_service = False return args @@ -49,11 +50,11 @@ def test_create_configuration(configuration: Configuration, user: User, mocker: set_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option") write_mock = mocker.patch("ahriman.core.configuration.Configuration.write") - CreateUser.create_configuration(configuration, user, "salt") + CreateUser.create_configuration(configuration, user, "salt", False) write_mock.assert_called_once() set_mock.assert_has_calls([ mock.call("auth", "salt", pytest.helpers.anyvar(str)), - mock.call(section, user.username, user.password) + mock.call(section, user.username, pytest.helpers.anyvar(str)) ]) @@ -65,7 +66,7 @@ def test_create_configuration_not_loaded(configuration: Configuration, user: Use mocker.patch("pathlib.Path.open") write_mock = mocker.patch("ahriman.core.configuration.Configuration.write") - CreateUser.create_configuration(configuration, user, "salt") + CreateUser.create_configuration(configuration, user, "salt", False) write_mock.assert_not_called() @@ -78,8 +79,28 @@ def test_create_configuration_user_exists(configuration: Configuration, user: Us mocker.patch("pathlib.Path.open") mocker.patch("ahriman.core.configuration.Configuration.write") - CreateUser.create_configuration(configuration, user, "salt") - assert configuration.get(section, user.username) == user.password + CreateUser.create_configuration(configuration, user, "salt", False) + generated = User.from_option(user.username, configuration.get(section, user.username)) + assert generated.check_credentials(user.password, configuration.get("auth", "salt")) + + +def test_create_configuration_with_plain_password( + configuration: Configuration, + user: User, + mocker: MockerFixture) -> None: + """ + must set plain text password and user for the service + """ + section = Configuration.section_name("auth", user.access.value) + mocker.patch("pathlib.Path.open") + mocker.patch("ahriman.core.configuration.Configuration.write") + + CreateUser.create_configuration(configuration, user, "salt", True) + + generated = User.from_option(user.username, configuration.get(section, user.username)) + service = User.from_option(configuration.get("web", "username"), configuration.get("web", "password")) + assert generated.username == service.username + assert generated.check_credentials(service.password, configuration.get("auth", "salt")) def test_create_user(args: argparse.Namespace, user: User) -> None: @@ -87,9 +108,8 @@ def test_create_user(args: argparse.Namespace, user: User) -> None: must create user """ args = _default_args(args) - generated = CreateUser.create_user(args, "salt") + generated = CreateUser.create_user(args) assert generated.username == user.username - assert generated.check_credentials(user.password, "salt") assert generated.access == user.access @@ -101,10 +121,10 @@ def test_create_user_getpass(args: argparse.Namespace, mocker: MockerFixture) -> args.password = None getpass_mock = mocker.patch("getpass.getpass", return_value="password") - generated = CreateUser.create_user(args, "salt") + generated = CreateUser.create_user(args) getpass_mock.assert_called_once() - assert generated.check_credentials("password", "salt") + assert generated.password == "password" def test_get_salt_read(configuration: Configuration) -> None: diff --git a/tests/ahriman/core/auth/test_mapping_auth.py b/tests/ahriman/core/auth/test_mapping_auth.py index e6fe1972..b3b11a91 100644 --- a/tests/ahriman/core/auth/test_mapping_auth.py +++ b/tests/ahriman/core/auth/test_mapping_auth.py @@ -58,7 +58,7 @@ def test_check_credentials(mapping_auth: MappingAuth, user: User) -> None: must return true for valid credentials """ current_password = user.password - user.password = user.hash_password(user.password, mapping_auth.salt) + user.password = user.hash_password(mapping_auth.salt) mapping_auth._users[user.username] = user assert mapping_auth.check_credentials(user.username, current_password) assert not mapping_auth.check_credentials(user.username, user.password) # here password is hashed so it is invalid diff --git a/tests/ahriman/models/test_user.py b/tests/ahriman/models/test_user.py index 7fd29048..3610e3a2 100644 --- a/tests/ahriman/models/test_user.py +++ b/tests/ahriman/models/test_user.py @@ -26,7 +26,7 @@ def test_check_credentials_hash_password(user: User) -> None: must generate and validate user password """ current_password = user.password - user.password = user.hash_password(current_password, "salt") + user.password = user.hash_password("salt") assert user.check_credentials(current_password, "salt") assert not user.check_credentials(current_password, "salt1") assert not user.check_credentials(user.password, "salt") diff --git a/tests/ahriman/web/conftest.py b/tests/ahriman/web/conftest.py index 57d26139..f6919ed5 100644 --- a/tests/ahriman/web/conftest.py +++ b/tests/ahriman/web/conftest.py @@ -37,7 +37,7 @@ def application_with_auth(configuration: Configuration, user: User, mocker: Mock mocker.patch("pathlib.Path.mkdir") application = setup_service("x86_64", configuration) - generated = User(user.username, user.hash_password(user.password, application["validator"].salt), user.access) + generated = User(user.username, user.hash_password(application["validator"].salt), user.access) application["validator"]._users[generated.username] = generated return application