mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-11-04 07:43:42 +00:00 
			
		
		
		
	add option to set user-password for service when updating its hash
This commit is contained in:
		@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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:
 | 
				
			||||||
 | 
				
			|||||||
@ -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:
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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")
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user