diff --git a/changelogs/fragments/122-implement-password-sanitation-before-hashing.yml b/changelogs/fragments/122-implement-password-sanitation-before-hashing.yml new file mode 100644 index 00000000..89a64aa3 --- /dev/null +++ b/changelogs/fragments/122-implement-password-sanitation-before-hashing.yml @@ -0,0 +1,7 @@ +--- +bugfixes: + - system_access_users - Introduced password sanitization to fix parsing errors. + - system_access_users - Introduced password verification to fix passwords not being updated. + +minor_changes: + - system_access_users - Enhanced group removal handling diff --git a/molecule/system_access_users/converge.yml b/molecule/system_access_users/converge.yml index 2093e320..605ed2dd 100644 --- a/molecule/system_access_users/converge.yml +++ b/molecule/system_access_users/converge.yml @@ -7,13 +7,13 @@ ansible.builtin.debug: msg: "test" - # Test User minimum requirements + # Test User minimum requirements - name: "Test User 1: Test minimum requirements User Creation" puzzle.opnsense.system_access_users: username: test_user_1 password: test_password_1 - # Test User minimum requirements disabled + # Test User minimum requirements disabled - name: "Test User 2: Test disabled User Creation" puzzle.opnsense.system_access_users: username: test_user_2 @@ -21,14 +21,14 @@ full_name: "Test User 2: Test disabled User Creation" disabled: True - # Test User with Full Name + # Test User with Full Name - name: "Test User 3: Test User Creation with Full Name" puzzle.opnsense.system_access_users: username: test_user_3 password: test_password_3 full_name: "Test User 3: Test User Creation with Full Name" - # Test User with E-Mail + # Test User with E-Mail - name: "Test User 4: Test User Creation with E-Mail" puzzle.opnsense.system_access_users: username: test_user_4 @@ -36,7 +36,7 @@ email: test_user_4@test.ch full_name: "Test User 4: Test User Creation with E-Mail" - # Test User with Comment + # Test User with Comment - name: "Test User 5: Test User Creation with Comment" puzzle.opnsense.system_access_users: username: test_user_5 @@ -44,7 +44,7 @@ comment: Test User 5 Comment full_name: "Test User 5: Test User Creation with Comment" - # Test User with Preferred landing page + # Test User with Preferred landing page - name: "Test User 6: Test User Creation with Preferred landing page" puzzle.opnsense.system_access_users: username: test_user_6 @@ -52,7 +52,7 @@ landing_page: /ui/ipsec/sessions full_name: "Test User 6: Test User Creation with Preferred landing page" - # Test User with nologin shell + # Test User with nologin shell - name: "Test User 7: Test User Creation with nologin shell" puzzle.opnsense.system_access_users: username: test_user_7 @@ -60,7 +60,7 @@ shell: /sbin/nologin full_name: "Test User 7: Test User Creation with nologin shell" - # Test User with csh shell + # Test User with csh shell - name: "Test User 8: Test User Creation with csh shell" puzzle.opnsense.system_access_users: username: test_user_8 @@ -68,7 +68,7 @@ shell: /bin/csh full_name: "Test User 8: Test User Creation with csh shell" - # Test User with sh shell + # Test User with sh shell - name: "Test User 9: Test User Creation with sh shell" puzzle.opnsense.system_access_users: username: test_user_9 @@ -76,7 +76,7 @@ shell: /bin/sh full_name: "Test User 9: Test User Creation with sh shell" - # Test User with tcsh shell + # Test User with tcsh shell - name: "Test User 10: Test User Creation with tcsh shell" puzzle.opnsense.system_access_users: username: test_user_10 @@ -84,7 +84,7 @@ shell: /bin/tcsh full_name: "Test User 10: Test User Creation with tcsh shell" - # Test User with Expiration date + # Test User with Expiration date - name: "Test User 11: Test User Creation with Expiration date" puzzle.opnsense.system_access_users: username: test_user_11 @@ -92,7 +92,7 @@ expires: 02/27/2024 full_name: "Test User 11: Test User Creation with Expiration date" - # Test User with group as string + # Test User with group as string - name: "Test User 12: Test User Creation with group as string" puzzle.opnsense.system_access_users: username: test_user_12 @@ -100,34 +100,34 @@ full_name: "Test User 12: Test User Creation with group as string" groups: admins - # Test User with group as list + # Test User with group as list - name: "Test User 13: Test User Creation with group as list" puzzle.opnsense.system_access_users: username: test_user_13 password: test_password_13 full_name: "Test User 13: Test User Creation with group as list" groups: - - admins + - admins - # Test User with not existing group as list + # Test User with not existing group as list - name: "Test User 14: Test User Creation with not existing group as list" puzzle.opnsense.system_access_users: username: test_user_14 password: test_password_14 full_name: "Test User 14: Test User Creation with not existing group as list" groups: - - test + - test register: test_user_14_result ignore_errors: yes - name: "Verify that the user creation failed due to non-existing group" ansible.builtin.assert: that: - - test_user_14_result is failed + - test_user_14_result is failed fail_msg: "User creation should fail due to non-existing group" success_msg: "User creation failed as expected due to non-existing group" - # Test User with empty otp_seed + # Test User with empty otp_seed - name: "Test User 15: Test User Creation with empty otp_seed" puzzle.opnsense.system_access_users: username: test_user_15 @@ -135,7 +135,7 @@ otp_seed: "" full_name: "Test User 15: Test User Creation with empty otp_seed" - # Test User with otp_seed + # Test User with otp_seed - name: "Test User 16: Test User Creation with otp_seed" puzzle.opnsense.system_access_users: username: test_user_16 @@ -143,7 +143,7 @@ otp_seed: test_seed full_name: "Test User 16: Test User Creation with otp_seed" - # Test User with empty authorizedkeys + # Test User with empty authorizedkeys - name: "Test User 17: Test User Creation with empty authorizedkeys" puzzle.opnsense.system_access_users: username: test_user_17 @@ -151,21 +151,21 @@ authorizedkeys: "" full_name: "Test User 17: Test User Creation with empty authorizedkeys" - # Test User with authorizedkeys + # Test User with authorizedkeys - name: "Test User 18: Test User Creation with authorizedkeys" puzzle.opnsense.system_access_users: - username: test_user_18 - password: test_password_18 - authorizedkeys: test_authorized_key - full_name: "Test User 18: Test User Creation with authorizedkeys" + username: test_user_18 + password: test_password_18 + authorizedkeys: test_authorized_key + full_name: "Test User 18: Test User Creation with authorizedkeys" - # Test User with empty api_keys + # Test User with empty api_keys - name: "Test User 19: Test User Creation with empty api_keys" puzzle.opnsense.system_access_users: - username: test_user_19 - password: test_password_19 - apikeys: "" - full_name: "Test User 19: Test User Creation with empty api_keys" + username: test_user_19 + password: test_password_19 + apikeys: "" + full_name: "Test User 19: Test User Creation with empty api_keys" register: api_keys_result - name: Return the created apikeys and secret of Test User 19 @@ -175,30 +175,30 @@ - "'generated_apikeys' in api_keys_result" - api_keys_result.generated_apikeys | length > 0 - # Test User with too short api_keys + # Test User with too short api_keys - name: "Test User 20: Test User Creation with too short api_keys" puzzle.opnsense.system_access_users: - username: test_user_20 - password: test_password_20 - apikeys: "TEST_API_KEY" - full_name: "Test User 20: Test User Creation with too short api_keys" + username: test_user_20 + password: test_password_20 + apikeys: "TEST_API_KEY" + full_name: "Test User 20: Test User Creation with too short api_keys" register: test_user_20_result ignore_errors: yes - name: "Verify that the user creation failed due to too short api key" ansible.builtin.assert: that: - - test_user_20_result is failed + - test_user_20_result is failed fail_msg: "The API key: TEST_API_KEY is not a valid string. Must be >= 80 characters." success_msg: "The API key: TEST_API_KEY is not a valid string. Must be >= 80 characters." - # Test User with valid api_keys + # Test User with valid api_keys - name: "Test User 21: Test User Creation with valid api_keys" puzzle.opnsense.system_access_users: - username: test_user_21 - password: test_password_21 - apikeys: "TEST_API_KEY_WITH_RANDOM_CHARS_UNTIL_80_zo5Y3bUpOQFfbQnAOB6GqbHsPAP9Jqbjofnqu9xc" - full_name: "Test User 21: Test User Creation with valid api_keys" + username: test_user_21 + password: test_password_21 + apikeys: "TEST_API_KEY_WITH_RANDOM_CHARS_UNTIL_80_zo5Y3bUpOQFfbQnAOB6GqbHsPAP9Jqbjofnqu9xc" + full_name: "Test User 21: Test User Creation with valid api_keys" register: api_keys_result - name: Return the created apikeys and secret of Test User 21 @@ -206,4 +206,36 @@ msg: "The following api_keys were created {{ api_keys_result.generated_apikeys }}" when: - "'generated_apikeys' in api_keys_result" - - api_keys_result.generated_apikeys | length > 0 \ No newline at end of file + - api_keys_result.generated_apikeys | length > 0 + + # Test User password escaping + - name: "Test User 22: Test password escaping" + puzzle.opnsense.system_access_users: + username: test_user_22 + password: test_password_22\ + shell: /bin/sh + groups: + - admins + + # Test User password escaping + - name: "Test User 23: Test password escaping" + puzzle.opnsense.system_access_users: + username: test_user_23 + password: test_password_23' + shell: /bin/sh + groups: + - admins + + # we have no alternative way to compare the values + # other than getting them from the config + # see https://github.com/opnsense/core/blob/24.1/src/opnsense/scripts/syslog/log_archive#L36 + - name: Get current config + ansible.builtin.slurp: + src: /conf/config.xml + register: current_config + + - name: Test that no error message is in config + ansible.builtin.assert: + that: + - "'syntax error, unexpected identifier \"cost\", expecting \")\" in Command line code on line 1' not in (current_config.content | b64decode | string)" + - "'syntax error, unexpected single-quoted string \",PASSWORD_BCRYPT,[ \", expecting \")\" in Command line code on line 1' not in (current_config.content | b64decode | string)" diff --git a/plugins/module_utils/system_access_users_utils.py b/plugins/module_utils/system_access_users_utils.py index 33e0de1e..7d844e59 100644 --- a/plugins/module_utils/system_access_users_utils.py +++ b/plugins/module_utils/system_access_users_utils.py @@ -63,6 +63,46 @@ class OPNSenseCryptReturnError(Exception): """ +class OPNSensePasswordVerifyReturnError(Exception): + """ + Exception raised when the return value of the instance is not what is expected + """ + + +def password_verify(existing_user_password: str, password: Optional[str]) -> bool: + """ + Verify if provided password matches the stored password using OPNsense's PHP command. + + Args: + existing_user_password (str): The hashed password stored in the XML config. + password (str): The plaintext password to verify. + + Returns: + bool: True if passwords match, False otherwise. + + Raises: + OPNSensePasswordVerifyReturnError: If an error occurs during verification. + """ + + if password is None: + return False + + # check if current password matches hash + password_matches = opnsense_utils.run_command( + php_requirements=[], + command=f"password_verify('{password}','{existing_user_password}');", + ) + + if password_matches.get("stderr"): + raise OPNSensePasswordVerifyReturnError("error encounterd verifying password") + + # if return code of password_matches not equals 1, it's a match + if password_matches.get("stdout") != "1": + return True + + return False + + @dataclass class Group: """ @@ -175,7 +215,7 @@ class User: Args: name (str): The username of the user. - password (str): The user's password. + password (Optional[str]): The user's password. scope (Optional[str]): The scope of the user, default is "User". descr (Optional[str]): A description of the user, if available. ipsecpsk (Optional[str]): IPsec pre-shared key, if applicable. @@ -210,7 +250,7 @@ class User: """ name: str - password: str + password: Optional[str] = None scope: Optional[str] = "User" descr: Optional[str] = None ipsecpsk: Optional[str] = None @@ -245,10 +285,22 @@ def __eq__(self, other) -> bool: if not isinstance(other, User): return False + if not hasattr(self, "password") or not hasattr(other, "password"): + return False + for _field in fields(self): - if _field.name not in ["password", "uid", "otp_seed", "apikeys"]: - if getattr(self, _field.name) != getattr(other, _field.name): - return False + if _field.name in ["uid", "otp_seed", "apikeys"]: + continue + + if _field.name == "password" and not password_verify( + existing_user_password=getattr(other, _field.name), + password=self.password, + ): + return False + + # if value is not equal return False + if getattr(self, _field.name) != getattr(other, _field.name): + return False return True @@ -383,7 +435,6 @@ def to_etree(self) -> Element: user_dict: dict = asdict(self) for user_key, user_val in user_dict.copy().items(): - if user_val is None and user_key in [ "expires", "ipsecpsk", @@ -690,6 +741,10 @@ def _update_user_groups(self, user: User, existing_user: Optional[User] = None): for existing_group in self._groups: if existing_group.check_if_user_in_group(target_user): existing_group.remove_user(target_user) + if target_user.groupname: + target_user.groupname.remove(existing_group.name) + if not target_user.groupname: + target_user.groupname = None return # Exit the method after removing the user from all groups. # Convert groupname to a list if it's not already. @@ -727,10 +782,13 @@ def set_user_password(self, user: User) -> None: "configure_params" ] + # sanitize and escape password + escaped_password = user.password.replace("\\", "\\\\").replace("'", "\\'") + # format parameters formatted_params = [ ( - param.replace("'password'", f"'{user.password}'") + param.replace("'password'", f"'{escaped_password}'") if "password" in param else param ) @@ -780,28 +838,37 @@ def add_or_update(self, user: User) -> None: ) next_uid: Element = self.get("uid") - # since the current password of an user cannot not be compared with the new one, - # we're setting the password anyways - self.set_user_password(user) - if existing_user: + if not password_verify( + existing_user_password=existing_user.password, password=user.password + ): + self.set_user_password(user) + + # since we don't want the clear-type password to be set, + # and it is clear a update is not needed, we remove it from the update + if "password" in user.__dict__: + del user.__dict__["password"] + # Update groups if needed self._update_user_groups(user, existing_user) # Update existing user's attributes existing_user.__dict__.update(user.__dict__) - else: - # Assign UID if not set - if not user.uid: - user.uid = next_uid.text - # Increase the next_uid - self.set(value=str(int(next_uid.text) + 1), setting="uid") - - if user.groupname: - # Update groups for the new user - self._update_user_groups(user) - # Add the new user - self._users.append(user) + + return + + self.set_user_password(user) + # Assign UID if not set + if not user.uid: + user.uid = next_uid.text + # Increase the next_uid + self.set(value=str(int(next_uid.text) + 1), setting="uid") + + if user.groupname: + # Update groups for the new user + self._update_user_groups(user) + # Add the new user + self._users.append(user) def delete(self, user: User) -> None: """ diff --git a/tests/unit/plugins/module_utils/test_system_access_users_utils.py b/tests/unit/plugins/module_utils/test_system_access_users_utils.py index eb363210..539f30b8 100644 --- a/tests/unit/plugins/module_utils/test_system_access_users_utils.py +++ b/tests/unit/plugins/module_utils/test_system_access_users_utils.py @@ -15,8 +15,10 @@ Group, User, UserSet, + password_verify, OPNSenseGroupNotFoundError, OPNSenseCryptReturnError, + OPNSensePasswordVerifyReturnError, ) from ansible_collections.puzzle.opnsense.plugins.module_utils.module_index import ( VERSION_MAP, @@ -87,6 +89,20 @@ /bin/sh 1001 + + test_user_23 + $2y$11$FGohY592rylJdDw5vTaxNubYHwh9326Eb7gtdY4GRbXrViGsPEykq + User + [ ANSIBLE ] + + + /bin/sh + 2021 + [ ANSIBLE ] + + + test_group + admins System Administrators @@ -108,6 +124,7 @@ system 1000 2004 + 2021 2000 page-all @@ -234,17 +251,23 @@ def test_user_from_ansible_module_params_simple(sample_config_path): "ansible_collections.puzzle.opnsense.plugins.module_utils.version_utils.get_opnsense_version", return_value="OPNsense Test", ) +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.password_verify", + return_value=True, +) @patch.dict(in_dict=VERSION_MAP, values=TEST_VERSION_MAP, clear=True) -def test_user_set_load_simple_user(mocked_version_utils: MagicMock, sample_config_path): +def test_user_set_load_simple_user( + mocked_version_utils: MagicMock, mock_password_verify: MagicMock, sample_config_path +): with UserSet(sample_config_path) as user_set: - assert len(user_set._users) == 2 + assert len(user_set._users) == 3 user_set.save() def test_group_from_xml(): test_etree_opnsense: Element = ElementTree.fromstring(TEST_XML) - test_etree_group: Element = list(list(test_etree_opnsense)[0])[4] + test_etree_group: Element = list(list(test_etree_opnsense)[0])[5] test_group: Group = Group.from_xml(test_etree_group) assert test_group.name == "admins" @@ -271,9 +294,16 @@ def test_group_from_xml(): "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.UserSet.set_user_password", return_value="$2y$10$1BvUdvwM.a.dJACwfeNfAOgNT6Cqc4cKZ2F6byyvY8hIK9I8fn36O", ) +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.password_verify", + return_value=True, +) @patch.dict(in_dict=VERSION_MAP, values=TEST_VERSION_MAP, clear=True) def test_user_set_add_group( - mocked_version_utils: MagicMock, mock_set_password: MagicMock, sample_config_path + mocked_version_utils: MagicMock, + mock_set_password: MagicMock, + mock_password_verify: MagicMock, + sample_config_path, ): with UserSet(sample_config_path) as user_set: test_user: User = user_set.find(name="vagrant") @@ -287,7 +317,6 @@ def test_user_set_add_group( with UserSet(sample_config_path) as new_user_set: new_test_user: User = new_user_set.find(name="vagrant") - # group: Group = new_user_set assert new_test_user.groupname == ["admins"] assert "1000" in new_user_set._groups[0].member @@ -328,9 +357,16 @@ def test_user_from_ansible_module_params_with_group(sample_config_path): "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.UserSet.set_user_password", return_value="$2y$10$1BvUdvwM.a.dJACwfeNfAOgNT6Cqc4cKZ2F6byyvY8hIK9I8fn36O", ) +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.password_verify", + return_value=True, +) @patch.dict(in_dict=VERSION_MAP, values=TEST_VERSION_MAP, clear=True) def test_user_from_ansible_module_params_with_group_as_string( - mock_set_password, mock_get_version, sample_config_path + mock_set_password, + mock_get_version, + mock_password_verify: MagicMock, + sample_config_path, ): test_params = { "username": "test", @@ -368,9 +404,16 @@ def test_user_from_ansible_module_params_with_group_as_string( "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.UserSet.set_user_password", return_value="$2y$10$1BvUdvwM.a.dJACwfeNfAOgNT6Cqc4cKZ2F6byyvY8hIK9I8fn36O", ) +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.password_verify", + return_value=True, +) @patch.dict(in_dict=VERSION_MAP, values=TEST_VERSION_MAP, clear=True) def test_user_from_ansible_module_params_with_multiple_groups_as_list( - mock_set_password, mock_get_version, sample_config_path + mock_set_password, + mock_get_version, + mock_password_verify: MagicMock, + sample_config_path, ): test_params = { "username": "test", @@ -411,9 +454,16 @@ def test_user_from_ansible_module_params_with_multiple_groups_as_list( "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.UserSet.set_user_password", return_value="$2y$10$1BvUdvwM.a.dJACwfeNfAOgNT6Cqc4cKZ2F6byyvY8hIK9I8fn36O", ) +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.password_verify", + return_value=True, +) @patch.dict(in_dict=VERSION_MAP, values=TEST_VERSION_MAP, clear=True) def test_user_from_ansible_module_params_with_no_groups( - mock_set_password, mock_get_version, sample_config_path + mock_set_password, + mock_get_version, + mock_password_verify: MagicMock, + sample_config_path, ): test_params = { "username": "test", @@ -448,9 +498,16 @@ def test_user_from_ansible_module_params_with_no_groups( "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.UserSet.set_user_password", return_value="$2y$10$1BvUdvwM.a.dJACwfeNfAOgNT6Cqc4cKZ2F6byyvY8hIK9I8fn36O", ) +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.password_verify", + return_value=True, +) @patch.dict(in_dict=VERSION_MAP, values=TEST_VERSION_MAP, clear=True) def test_user_from_ansible_module_params_with_not_existing_group( - mock_set_password, mock_get_version, sample_config_path + mock_set_password, + mock_get_version, + mock_password_verify: MagicMock, + sample_config_path, ): test_params = { "username": "test", @@ -515,17 +572,24 @@ def test_user_from_ansible_module_params_with_authorizedkeys( "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.UserSet.set_user_password", return_value="$2y$10$1BvUdvwM.a.dJACwfeNfAOgNT6Cqc4cKZ2F6byyvY8hIK9I8fn36O", ) +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.password_verify", + return_value=True, +) @patch.dict(in_dict=VERSION_MAP, values=TEST_VERSION_MAP, clear=True) def test_user_from_ansible_module_params_single_group_removal( - mock_set_password, mock_get_version, sample_config_path + mock_set_password, + mock_get_version, + mock_password_verify: MagicMock, + sample_config_path, ): test_params = { - "username": "vagrant", - "password": "vagrant", + "username": "test_user_23", + "password": "test_password_23", "scope": "user", - "full_name": "vagrant box management", + "full_name": "[ ANSIBLE ]", "shell": "/bin/sh", - "uid": "1000", + "uid": "2021", } with UserSet(sample_config_path) as user_set: @@ -538,10 +602,12 @@ def test_user_from_ansible_module_params_single_group_removal( with UserSet(sample_config_path) as new_user_set: all_groups = new_user_set._load_groups() + test_user: User = user_set.find(name="test_user_23") - admin_group = all_groups[0] + test_group = all_groups[1] - assert "1000" not in admin_group.member + assert "2021" not in test_group.member + assert not test_user.groupname new_user_set.save() @@ -554,9 +620,16 @@ def test_user_from_ansible_module_params_single_group_removal( "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.UserSet.set_user_password", return_value="$2y$10$1BvUdvwM.a.dJACwfeNfAOgNT6Cqc4cKZ2F6byyvY8hIK9I8fn36O", ) +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.system_access_users_utils.password_verify", + return_value=True, +) @patch.dict(in_dict=VERSION_MAP, values=TEST_VERSION_MAP, clear=True) def test_user_from_ansible_module_params_multiple_group_removal( - mock_set_password, mock_get_version, sample_config_path + mock_set_password, + mock_get_version, + mock_password_verify: MagicMock, + sample_config_path, ): test_params = { "username": "vagrant", @@ -631,3 +704,68 @@ def test_generate_hashed_secret_error_in_crypt(mock_run_function): user._generate_hashed_secret("password123") assert "error encounterd while creating secret" in str(excinfo.value) + + +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.opnsense_utils.run_command" +) +def test_password_verify_returns_true_on_match(mock_run_command: MagicMock): + + # Mock the return value of the run_command to simulate a password match + mock_run_command.return_value = { + "stdout": "", + "stderr": None, + } + + # Call the function with test data + test_password_matches = password_verify( + password="test_password_1", + existing_user_password="$2y$11$pSYTZcD0o23JSfksEekwKOnWM1o3Ih9vp7OOQN.v35E1rag49cEc6", + ) + + # Assert that the function returns True for a password match + assert test_password_matches + + +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.opnsense_utils.run_command" +) +def test_password_verify_returns_false_on_difference(mock_run_command: MagicMock): + + # Mock the return value of the run_command to simulate a password match + mock_run_command.return_value = { + "stdout": "1", + "stderr": None, + } + + # Call the function with test data + test_password_matches = password_verify( + password="test_password_1", + existing_user_password="$2y$11$pSYTZcD0o23JSfksEe1231345h9vp7OOQN.v35E1rag49cEc6", + ) + + # Assert that the function returns True for a password match + assert not test_password_matches + + +@patch( + "ansible_collections.puzzle.opnsense.plugins.module_utils.opnsense_utils.run_command" +) +def test_password_verify_returns_OPNSensePasswordVerifyReturnError( + mock_run_command: MagicMock, +): + + # Mock the return value of the run_command to simulate a password match + mock_run_command.return_value = { + "stdout": None, + "stderr": "this an error", + } + + with pytest.raises(OPNSensePasswordVerifyReturnError) as excinfo: + # Call the function with test data + test_password_matches = password_verify( + password="test_password_1", + existing_user_password="$2y$11$pSYTZcD0o23JSfksEekwKOnWM1o3Ih9vp7OOQN.v35E1rag49cEc6", + ) + + assert "error encounterd verifying passwor" in str(excinfo.value)