diff --git a/ironic_python_agent/extensions/rescue.py b/ironic_python_agent/extensions/rescue.py index 0769bd8fd..6ec8a2f99 100644 --- a/ironic_python_agent/extensions/rescue.py +++ b/ironic_python_agent/extensions/rescue.py @@ -35,14 +35,28 @@ class RescueExtension(base.BaseAgentExtension): allowed_chars = string.ascii_letters + string.digits return random.choice(allowed_chars) + random.choice(allowed_chars) - def write_rescue_password(self, rescue_password=""): + def write_rescue_password(self, rescue_password="", hashed=False): """Write rescue password to a file for use after IPA exits. :param rescue_password: Rescue password. + :param hashed: Boolean default False indicating if the password + being provided is hashed or not. This will be changed + in a future version of ironic. """ + # DEPRECATED(TheJulia): In a future version of the ramdisk, we need + # change the default such that a password is the default and that + # if it is not, then the operation fails. Providing a default and + # an override now that matches the present state allows us to + # maintain our n-1, n, and n+1 theoretical support. Change + # in the V or W cycles. LOG.debug('Writing hashed rescue password to %s', PASSWORD_FILE) - salt = self.make_salt() - hashed_password = crypt.crypt(rescue_password, salt) + password = str(rescue_password) + hashed_password = None + if hashed: + hashed_password = password + else: + salt = self.make_salt() + hashed_password = crypt.crypt(rescue_password, salt) try: with open(PASSWORD_FILE, 'w') as f: f.write(hashed_password) @@ -53,9 +67,9 @@ class RescueExtension(base.BaseAgentExtension): raise IOError(msg) @base.sync_command('finalize_rescue') - def finalize_rescue(self, rescue_password=""): + def finalize_rescue(self, rescue_password="", hashed=False): """Sets the rescue password for the rescue user.""" - self.write_rescue_password(rescue_password) + self.write_rescue_password(rescue_password, hashed) # IPA will terminate after the result of finalize_rescue is returned to # ironic to avoid exposing the IPA API to a tenant or public network self.agent.serve_api = False diff --git a/ironic_python_agent/tests/unit/extensions/test_rescue.py b/ironic_python_agent/tests/unit/extensions/test_rescue.py index a416f2272..c7b4148b4 100644 --- a/ironic_python_agent/tests/unit/extensions/test_rescue.py +++ b/ironic_python_agent/tests/unit/extensions/test_rescue.py @@ -66,6 +66,38 @@ class TestRescueExtension(test_base.BaseTestCase): IOError, self.agent_extension.write_rescue_password, 'password') + @mock.patch('ironic_python_agent.extensions.rescue.crypt.crypt', + autospec=True) + @mock.patch('ironic_python_agent.extensions.rescue.RescueExtension.' + 'make_salt', autospec=True) + def _write_password_hashed_test(self, password, mock_salt, + mock_crypt): + mock_open = mock.mock_open() + with mock.patch('ironic_python_agent.extensions.rescue.open', + mock_open): + self.agent_extension.write_rescue_password(password, + hashed=True) + self.assertFalse(mock_salt.called) + self.assertFalse(mock_crypt.called) + mock_open.assert_called_once_with( + '/etc/ipa-rescue-config/ipa-rescue-password', 'w') + file_handle = mock_open() + file_handle.write.assert_called_once_with(password) + + def test_hashed_passwords(self): + # NOTE(TheJulia): Sort of redundant in that we're not actually + # verifying content here, but these are semi-realistic values + # that may be passed in, so best to just keep it regardless. + passwds = ['$1$1234567890234567890123456789001', + '$2a$012345678901234566789012345678901234567890123' + '45678901234', + '$5$1234567890123456789012345678901234567890123456' + '789012', + '$6$1234567890123456789012345678901234567890123456' + '7890123456789012345678901234567890123456789012345'] + for passwd in passwds: + self._write_password_hashed_test(passwd) + @mock.patch('ironic_python_agent.extensions.rescue.RescueExtension.' 'write_rescue_password', autospec=True) def test_finalize_rescue(self, mock_write_rescue_password): @@ -73,5 +105,5 @@ class TestRescueExtension(test_base.BaseTestCase): self.agent_extension.finalize_rescue(rescue_password='password') mock_write_rescue_password.assert_called_once_with( mock.ANY, - rescue_password='password') + rescue_password='password', hashed=False) self.assertFalse(self.agent_extension.agent.serve_api) diff --git a/releasenotes/notes/permit-pre-hashed-rescue-passwords-4275f6e697533cec.yaml b/releasenotes/notes/permit-pre-hashed-rescue-passwords-4275f6e697533cec.yaml new file mode 100644 index 000000000..d4155c33a --- /dev/null +++ b/releasenotes/notes/permit-pre-hashed-rescue-passwords-4275f6e697533cec.yaml @@ -0,0 +1,6 @@ +--- +security: + - | + Enables pre-hashed passwords to be supplied to the ``rescue`` extension. + See `story 2006777 `_ + for more information.