From 040d86e03f1b6f21cd652c4bb21e7981cc600ddd Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Tue, 8 Sep 2015 12:12:18 +0300 Subject: [PATCH] Move the logic from windows.setuserpassword back into common.setuserpassword The password can be forced to be changed at the next login for Unix systems as well, so having a specific Windows plugin makes no sense. This means that change_password_next_logon becomes a new method in the base osutils. Change-Id: I0e470a10177e17c7817ee02abee25ed0b7247301 --- cloudbaseinit/osutils/base.py | 4 ++ cloudbaseinit/osutils/windows.py | 2 +- cloudbaseinit/plugins/common/factory.py | 6 +- .../plugins/common/setuserpassword.py | 39 +++++++++-- .../plugins/windows/setuserpassword.py | 65 ----------------- .../plugins/common/test_setuserpassword.py | 58 ++++++++++++++-- .../plugins/windows/test_setuserpassword.py | 69 ------------------- 7 files changed, 95 insertions(+), 148 deletions(-) delete mode 100644 cloudbaseinit/plugins/windows/setuserpassword.py delete mode 100644 cloudbaseinit/tests/plugins/windows/test_setuserpassword.py diff --git a/cloudbaseinit/osutils/base.py b/cloudbaseinit/osutils/base.py index 0558b03f..192591ec 100644 --- a/cloudbaseinit/osutils/base.py +++ b/cloudbaseinit/osutils/base.py @@ -114,3 +114,7 @@ class BaseOSUtils(object): def set_timezone(self, timezone): """Set the timezone for this instance.""" raise NotImplementedError() + + def change_password_next_logon(self, username): + """Force the given user to change his password at the next login.""" + raise NotImplementedError() diff --git a/cloudbaseinit/osutils/windows.py b/cloudbaseinit/osutils/windows.py index 58c8702d..51dd8e7a 100644 --- a/cloudbaseinit/osutils/windows.py +++ b/cloudbaseinit/osutils/windows.py @@ -1080,7 +1080,7 @@ class WindowsUtils(base.BaseOSUtils): timezone.Timezone(windows_name).set(self) def change_password_next_logon(self, username): - """Force the given user to change the password at next logon.""" + """Force the given user to change the password at next login.""" user = self._get_adsi_object(object_name=username, object_type='user') user.Put('PasswordExpired', self.PASSWORD_CHANGED_FLAG) diff --git a/cloudbaseinit/plugins/common/factory.py b/cloudbaseinit/plugins/common/factory.py index ed62b9c1..22c2100b 100644 --- a/cloudbaseinit/plugins/common/factory.py +++ b/cloudbaseinit/plugins/common/factory.py @@ -32,7 +32,7 @@ opts = [ 'SetUserSSHPublicKeysPlugin', 'cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin', 'cloudbaseinit.plugins.common.userdata.UserDataPlugin', - 'cloudbaseinit.plugins.windows.setuserpassword.' + 'cloudbaseinit.plugins.common.setuserpassword.' 'SetUserPasswordPlugin', 'cloudbaseinit.plugins.windows.winrmlistener.' 'ConfigWinRMListenerPlugin', @@ -72,8 +72,8 @@ OLD_PLUGINS = { 'cloudbaseinit.plugins.windows.userdata.UserDataPlugin': 'cloudbaseinit.plugins.common.userdata.UserDataPlugin', - 'cloudbaseinit.plugins.common.setuserpassword.SetUserPasswordPlugin': - 'cloudbaseinit.plugins.windows.setuserpassword.SetUserPasswordPlugin', + 'cloudbaseinit.plugins.windows.setuserpassword.SetUserPasswordPlugin': + 'cloudbaseinit.plugins.common.setuserpassword.SetUserPasswordPlugin', 'cloudbaseinit.plugins.windows.localscripts.LocalScriptsPlugin': 'cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin', diff --git a/cloudbaseinit/plugins/common/setuserpassword.py b/cloudbaseinit/plugins/common/setuserpassword.py index 4d881dcb..76fbceff 100644 --- a/cloudbaseinit/plugins/common/setuserpassword.py +++ b/cloudbaseinit/plugins/common/setuserpassword.py @@ -23,12 +23,31 @@ from cloudbaseinit.plugins.common import constants from cloudbaseinit.utils import crypt +CLEAR_TEXT_INJECTED_ONLY = 'clear_text_injected_only' +ALWAYS_CHANGE = 'always' +NEVER_CHANGE = 'no' +LOGON_PASSWORD_CHANGE_OPTIONS = [ + CLEAR_TEXT_INJECTED_ONLY, + NEVER_CHANGE, + ALWAYS_CHANGE, +] opts = [ cfg.BoolOpt('inject_user_password', default=True, help='Set the password ' 'provided in the configuration. If False or no password is ' 'provided, a random one will be set'), + cfg.StrOpt('first_logon_behaviour', + default=CLEAR_TEXT_INJECTED_ONLY, + choices=LOGON_PASSWORD_CHANGE_OPTIONS, + help='Control the behaviour of what happens at ' + 'next logon. If this option is set to `always`, ' + 'then the user will be forced to change the password ' + 'at next logon. If it is set to ' + '`clear_text_injected_only`, ' + 'then the user will have to change the password only if ' + 'the password is a clear text password, coming from the ' + 'metadata. The last option is `no`, when the user is ' + 'never forced to change the password.'), ] - CONF = cfg.CONF CONF.register_opts(opts) CONF.import_opt('username', 'cloudbaseinit.plugins.common.createuser') @@ -103,15 +122,23 @@ class SetUserPasswordPlugin(base.BasePlugin): maximum_length) osutils.set_user_password(user_name, password) - self.post_set_password(user_name, password, - password_injected=injected) + self._change_logon_behaviour(user_name, password_injected=injected) return password - def post_set_password(self, username, password, password_injected=False): - """Executes post set password logic. + def _change_logon_behaviour(self, username, password_injected=False): + """Post set password logic - This is called by :meth:`execute` after the password was set. + If the option is activated, force the user to change the + password at next logon. """ + if CONF.first_logon_behaviour == NEVER_CHANGE: + return + + clear_text = CONF.first_logon_behaviour == CLEAR_TEXT_INJECTED_ONLY + always = CONF.first_logon_behaviour == ALWAYS_CHANGE + if always or (clear_text and password_injected): + osutils = osutils_factory.get_os_utils() + osutils.change_password_next_logon(username) def execute(self, service, shared_data): # TODO(alexpilotti): The username selection logic must be set in the diff --git a/cloudbaseinit/plugins/windows/setuserpassword.py b/cloudbaseinit/plugins/windows/setuserpassword.py deleted file mode 100644 index e2017625..00000000 --- a/cloudbaseinit/plugins/windows/setuserpassword.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2015 Cloudbase Solutions Srl -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from oslo_config import cfg - -from cloudbaseinit.osutils import factory -from cloudbaseinit.plugins.common import setuserpassword - -CLEAR_TEXT_INJECTED_ONLY = 'clear_text_injected_only' -ALWAYS_CHANGE = 'always' -NEVER_CHANGE = 'no' -LOGON_PASSWORD_CHANGE_OPTIONS = [ - CLEAR_TEXT_INJECTED_ONLY, - NEVER_CHANGE, - ALWAYS_CHANGE, -] - -opts = [ - cfg.StrOpt('first_logon_behaviour', - default=CLEAR_TEXT_INJECTED_ONLY, - choices=LOGON_PASSWORD_CHANGE_OPTIONS, - help='Control the behaviour of what happens at ' - 'next logon. If this option is set to `always`, ' - 'then the user will be forced to change the password ' - 'at next logon. If it is set to ' - '`clear_text_injected_only`, ' - 'then the user will have to change the password only if ' - 'the password is a clear text password, coming from the ' - 'metadata. The last option is `no`, when the user is ' - 'never forced to change the password.'), - -] - -CONF = cfg.CONF -CONF.register_opts(opts) - - -class SetUserPasswordPlugin(setuserpassword.SetUserPasswordPlugin): - """Plugin for changing the password, tailored to Windows.""" - - def post_set_password(self, username, _, password_injected=False): - """Post set password logic - - If the option is activated, force the user to change the - password at next logon. - """ - if CONF.first_logon_behaviour == NEVER_CHANGE: - return - - clear_text = CONF.first_logon_behaviour == CLEAR_TEXT_INJECTED_ONLY - always = CONF.first_logon_behaviour == ALWAYS_CHANGE - if always or (clear_text and password_injected): - osutils = factory.get_os_utils() - osutils.change_password_next_logon(username) diff --git a/cloudbaseinit/tests/plugins/common/test_setuserpassword.py b/cloudbaseinit/tests/plugins/common/test_setuserpassword.py index b4f81f3b..efab7978 100644 --- a/cloudbaseinit/tests/plugins/common/test_setuserpassword.py +++ b/cloudbaseinit/tests/plugins/common/test_setuserpassword.py @@ -156,10 +156,11 @@ class SetUserPasswordPluginTests(unittest.TestCase): self.assertEqual(expected_logging, snatcher.output) @mock.patch('cloudbaseinit.plugins.common.setuserpassword.' - 'SetUserPasswordPlugin.post_set_password') + 'SetUserPasswordPlugin._change_logon_behaviour') @mock.patch('cloudbaseinit.plugins.common.setuserpassword.' 'SetUserPasswordPlugin._get_password') - def _test_set_password(self, mock_get_password, mock_post_set_password, + def _test_set_password(self, mock_get_password, + mock_change_logon_behaviour, password, can_update_password, is_password_changed, injected=False): expected_password = password @@ -196,8 +197,8 @@ class SetUserPasswordPluginTests(unittest.TestCase): self.assertEqual(expected_password, response) self.assertEqual(expected_logging, snatcher.output) if password and can_update_password and is_password_changed: - mock_post_set_password.assert_called_once_with( - user, expected_password, password_injected=injected) + mock_change_logon_behaviour.assert_called_once_with( + user, password_injected=injected) def test_set_password(self): self._test_set_password(password='Password', @@ -268,3 +269,52 @@ class SetUserPasswordPluginTests(unittest.TestCase): self._test_execute(is_password_set=False, can_post_password=True) self._test_execute(is_password_set=True, can_post_password=True, can_update_password=True) + + @mock.patch.object(setuserpassword.osutils_factory, 'get_os_utils') + @testutils.ConfPatcher('first_logon_behaviour', + setuserpassword.NEVER_CHANGE) + def test_logon_behaviour_never_change(self, mock_get_os_utils): + self._setpassword_plugin._change_logon_behaviour( + mock.sentinel.username) + + self.assertFalse(mock_get_os_utils.called) + + @testutils.ConfPatcher('first_logon_behaviour', + setuserpassword.ALWAYS_CHANGE) + @mock.patch.object(setuserpassword, 'osutils_factory') + def test_logon_behaviour_always(self, mock_factory): + self._setpassword_plugin._change_logon_behaviour( + mock.sentinel.username) + + mock_get_os_utils = mock_factory.get_os_utils + self.assertTrue(mock_get_os_utils.called) + osutils = mock_get_os_utils.return_value + osutils.change_password_next_logon.assert_called_once_with( + mock.sentinel.username) + + @testutils.ConfPatcher('first_logon_behaviour', + setuserpassword.CLEAR_TEXT_INJECTED_ONLY) + @mock.patch.object(setuserpassword, 'osutils_factory') + def test_change_logon_behaviour_clear_text_password_not_injected( + self, mock_factory): + self._setpassword_plugin._change_logon_behaviour( + mock.sentinel.username, + password_injected=False) + + mock_get_os_utils = mock_factory.get_os_utils + self.assertFalse(mock_get_os_utils.called) + + @testutils.ConfPatcher('first_logon_behaviour', + setuserpassword.CLEAR_TEXT_INJECTED_ONLY) + @mock.patch.object(setuserpassword, 'osutils_factory') + def test_logon_behaviour_clear_text_password_injected( + self, mock_factory): + self._setpassword_plugin._change_logon_behaviour( + mock.sentinel.username, + password_injected=True) + + mock_get_os_utils = mock_factory.get_os_utils + self.assertTrue(mock_get_os_utils.called) + osutils = mock_get_os_utils.return_value + osutils.change_password_next_logon.assert_called_once_with( + mock.sentinel.username) diff --git a/cloudbaseinit/tests/plugins/windows/test_setuserpassword.py b/cloudbaseinit/tests/plugins/windows/test_setuserpassword.py deleted file mode 100644 index d68dd498..00000000 --- a/cloudbaseinit/tests/plugins/windows/test_setuserpassword.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2015 Cloudbase Solutions Srl -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import unittest - -import mock - -from cloudbaseinit.plugins.windows import setuserpassword -from cloudbaseinit.tests import testutils - - -@mock.patch.object(setuserpassword.factory, 'get_os_utils') -class TestSetUserPassword(unittest.TestCase): - - def setUp(self): - self._plugin = setuserpassword.SetUserPasswordPlugin() - - @testutils.ConfPatcher('first_logon_behaviour', - setuserpassword.NEVER_CHANGE) - def test_post_set_password_never_change(self, mock_get_os_utils): - self._plugin.post_set_password(mock.sentinel.username, - mock.sentinel.password) - - self.assertFalse(mock_get_os_utils.called) - - @testutils.ConfPatcher('first_logon_behaviour', - setuserpassword.ALWAYS_CHANGE) - def test_post_set_password_always(self, mock_get_os_utils): - self._plugin.post_set_password(mock.sentinel.username, - mock.sentinel.password) - - self.assertTrue(mock_get_os_utils.called) - osutils = mock_get_os_utils.return_value - osutils.change_password_next_logon.assert_called_once_with( - mock.sentinel.username) - - @testutils.ConfPatcher('first_logon_behaviour', - setuserpassword.CLEAR_TEXT_INJECTED_ONLY) - def test_post_set_password_clear_text_password_not_injected( - self, mock_get_os_utils): - self._plugin.post_set_password(mock.sentinel.username, - mock.sentinel.password, - password_injected=False) - - self.assertFalse(mock_get_os_utils.called) - - @testutils.ConfPatcher('first_logon_behaviour', - setuserpassword.CLEAR_TEXT_INJECTED_ONLY) - def test_post_set_password_clear_text_password_injected( - self, mock_get_os_utils): - self._plugin.post_set_password(mock.sentinel.username, - mock.sentinel.password, - password_injected=True) - - self.assertTrue(mock_get_os_utils.called) - osutils = mock_get_os_utils.return_value - osutils.change_password_next_logon.assert_called_once_with( - mock.sentinel.username)