add option to set user-password for service when updating its hash

This commit is contained in:
Evgenii Alekseev 2021-09-05 15:40:03 +03:00
parent 08e0237639
commit 18de70154e
7 changed files with 45 additions and 22 deletions

View File

@ -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", 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) formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("username", help="username for web service") 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("-r", "--role", help="user role", type=UserAccess, choices=UserAccess, default=UserAccess.Read)
parser.add_argument("-p", "--password", help="user password") parser.add_argument("-p", "--password", help="user password")
parser.set_defaults( parser.set_defaults(

View File

@ -42,21 +42,26 @@ class CreateUser(Handler):
:param configuration: configuration instance :param configuration: configuration instance
""" """
salt = CreateUser.get_salt(configuration) salt = CreateUser.get_salt(configuration)
user = CreateUser.create_user(args, salt) user = CreateUser.create_user(args)
auth_configuration = CreateUser.get_auth_configuration(configuration.include) 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 @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 put new user to configuration
:param configuration: configuration instance :param configuration: configuration instance
:param user: user descriptor :param user: user descriptor
:param salt: password hash salt :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) section = Configuration.section_name("auth", user.access.value)
configuration.set_option("auth", "salt", salt) 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: if configuration.path is None:
return return
@ -64,17 +69,15 @@ class CreateUser(Handler):
configuration.write(ahriman_configuration) configuration.write(ahriman_configuration)
@staticmethod @staticmethod
def create_user(args: argparse.Namespace, salt: str) -> User: def create_user(args: argparse.Namespace) -> User:
""" """
create user descriptor from arguments create user descriptor from arguments
:param args: command line args :param args: command line args
:param salt: password hash salt
:return: built user descriptor :return: built user descriptor
""" """
user = User(args.username, args.password, args.role) user = User(args.username, args.password, args.role)
if user.password is None: if user.password is None:
user.password = getpass.getpass() user.password = getpass.getpass()
user.password = user.hash_password(user.password, salt)
return user return user
@staticmethod @staticmethod

View File

@ -73,14 +73,13 @@ class User:
verified: bool = self._HASHER.verify(password + salt, self.password) verified: bool = self._HASHER.verify(password + salt, self.password)
return verified return verified
def hash_password(self, password: str, salt: str) -> str: def hash_password(self, salt: str) -> str:
""" """
generate hashed password from plain text generate hashed password from plain text
:param password: entered password
:param salt: salt for hashed password :param salt: salt for hashed password
:return: hashed string to store in configuration :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 return password_hash
def verify_access(self, required: UserAccess) -> bool: def verify_access(self, required: UserAccess) -> bool:

View File

@ -20,6 +20,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.username = "user" args.username = "user"
args.password = "pa55w0rd" args.password = "pa55w0rd"
args.role = UserAccess.Status args.role = UserAccess.Status
args.as_service = False
return args 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") set_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write") 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() write_mock.assert_called_once()
set_mock.assert_has_calls([ set_mock.assert_has_calls([
mock.call("auth", "salt", pytest.helpers.anyvar(str)), 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") mocker.patch("pathlib.Path.open")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write") 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() 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("pathlib.Path.open")
mocker.patch("ahriman.core.configuration.Configuration.write") mocker.patch("ahriman.core.configuration.Configuration.write")
CreateUser.create_configuration(configuration, user, "salt") CreateUser.create_configuration(configuration, user, "salt", False)
assert configuration.get(section, user.username) == user.password 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: 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 must create user
""" """
args = _default_args(args) args = _default_args(args)
generated = CreateUser.create_user(args, "salt") generated = CreateUser.create_user(args)
assert generated.username == user.username assert generated.username == user.username
assert generated.check_credentials(user.password, "salt")
assert generated.access == user.access assert generated.access == user.access
@ -101,10 +121,10 @@ def test_create_user_getpass(args: argparse.Namespace, mocker: MockerFixture) ->
args.password = None args.password = None
getpass_mock = mocker.patch("getpass.getpass", return_value="password") 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() getpass_mock.assert_called_once()
assert generated.check_credentials("password", "salt") assert generated.password == "password"
def test_get_salt_read(configuration: Configuration) -> None: def test_get_salt_read(configuration: Configuration) -> None:

View File

@ -58,7 +58,7 @@ def test_check_credentials(mapping_auth: MappingAuth, user: User) -> None:
must return true for valid credentials must return true for valid credentials
""" """
current_password = user.password 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 mapping_auth._users[user.username] = user
assert mapping_auth.check_credentials(user.username, current_password) 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 assert not mapping_auth.check_credentials(user.username, user.password) # here password is hashed so it is invalid

View File

@ -26,7 +26,7 @@ def test_check_credentials_hash_password(user: User) -> None:
must generate and validate user password must generate and validate user password
""" """
current_password = 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 user.check_credentials(current_password, "salt")
assert not user.check_credentials(current_password, "salt1") assert not user.check_credentials(current_password, "salt1")
assert not user.check_credentials(user.password, "salt") assert not user.check_credentials(user.password, "salt")

View File

@ -37,7 +37,7 @@ def application_with_auth(configuration: Configuration, user: User, mocker: Mock
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application = setup_service("x86_64", configuration) 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 application["validator"]._users[generated.username] = generated
return application return application