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
This commit is contained in:
Claudiu Popa 2015-09-08 12:12:18 +03:00
parent 6c121f5016
commit 040d86e03f
7 changed files with 95 additions and 148 deletions

View File

@ -114,3 +114,7 @@ class BaseOSUtils(object):
def set_timezone(self, timezone): def set_timezone(self, timezone):
"""Set the timezone for this instance.""" """Set the timezone for this instance."""
raise NotImplementedError() raise NotImplementedError()
def change_password_next_logon(self, username):
"""Force the given user to change his password at the next login."""
raise NotImplementedError()

View File

@ -1080,7 +1080,7 @@ class WindowsUtils(base.BaseOSUtils):
timezone.Timezone(windows_name).set(self) timezone.Timezone(windows_name).set(self)
def change_password_next_logon(self, username): 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, user = self._get_adsi_object(object_name=username,
object_type='user') object_type='user')
user.Put('PasswordExpired', self.PASSWORD_CHANGED_FLAG) user.Put('PasswordExpired', self.PASSWORD_CHANGED_FLAG)

View File

@ -32,7 +32,7 @@ opts = [
'SetUserSSHPublicKeysPlugin', 'SetUserSSHPublicKeysPlugin',
'cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin', 'cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin',
'cloudbaseinit.plugins.common.userdata.UserDataPlugin', 'cloudbaseinit.plugins.common.userdata.UserDataPlugin',
'cloudbaseinit.plugins.windows.setuserpassword.' 'cloudbaseinit.plugins.common.setuserpassword.'
'SetUserPasswordPlugin', 'SetUserPasswordPlugin',
'cloudbaseinit.plugins.windows.winrmlistener.' 'cloudbaseinit.plugins.windows.winrmlistener.'
'ConfigWinRMListenerPlugin', 'ConfigWinRMListenerPlugin',
@ -72,8 +72,8 @@ OLD_PLUGINS = {
'cloudbaseinit.plugins.windows.userdata.UserDataPlugin': 'cloudbaseinit.plugins.windows.userdata.UserDataPlugin':
'cloudbaseinit.plugins.common.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.windows.localscripts.LocalScriptsPlugin':
'cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin', 'cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin',

View File

@ -23,12 +23,31 @@ from cloudbaseinit.plugins.common import constants
from cloudbaseinit.utils import crypt 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 = [ opts = [
cfg.BoolOpt('inject_user_password', default=True, help='Set the password ' cfg.BoolOpt('inject_user_password', default=True, help='Set the password '
'provided in the configuration. If False or no password is ' 'provided in the configuration. If False or no password is '
'provided, a random one will be set'), '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 = cfg.CONF
CONF.register_opts(opts) CONF.register_opts(opts)
CONF.import_opt('username', 'cloudbaseinit.plugins.common.createuser') CONF.import_opt('username', 'cloudbaseinit.plugins.common.createuser')
@ -103,15 +122,23 @@ class SetUserPasswordPlugin(base.BasePlugin):
maximum_length) maximum_length)
osutils.set_user_password(user_name, password) osutils.set_user_password(user_name, password)
self.post_set_password(user_name, password, self._change_logon_behaviour(user_name, password_injected=injected)
password_injected=injected)
return password return password
def post_set_password(self, username, password, password_injected=False): def _change_logon_behaviour(self, username, password_injected=False):
"""Executes post set password logic. """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): def execute(self, service, shared_data):
# TODO(alexpilotti): The username selection logic must be set in the # TODO(alexpilotti): The username selection logic must be set in the

View File

@ -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)

View File

@ -156,10 +156,11 @@ class SetUserPasswordPluginTests(unittest.TestCase):
self.assertEqual(expected_logging, snatcher.output) self.assertEqual(expected_logging, snatcher.output)
@mock.patch('cloudbaseinit.plugins.common.setuserpassword.' @mock.patch('cloudbaseinit.plugins.common.setuserpassword.'
'SetUserPasswordPlugin.post_set_password') 'SetUserPasswordPlugin._change_logon_behaviour')
@mock.patch('cloudbaseinit.plugins.common.setuserpassword.' @mock.patch('cloudbaseinit.plugins.common.setuserpassword.'
'SetUserPasswordPlugin._get_password') '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, password, can_update_password,
is_password_changed, injected=False): is_password_changed, injected=False):
expected_password = password expected_password = password
@ -196,8 +197,8 @@ class SetUserPasswordPluginTests(unittest.TestCase):
self.assertEqual(expected_password, response) self.assertEqual(expected_password, response)
self.assertEqual(expected_logging, snatcher.output) self.assertEqual(expected_logging, snatcher.output)
if password and can_update_password and is_password_changed: if password and can_update_password and is_password_changed:
mock_post_set_password.assert_called_once_with( mock_change_logon_behaviour.assert_called_once_with(
user, expected_password, password_injected=injected) user, password_injected=injected)
def test_set_password(self): def test_set_password(self):
self._test_set_password(password='Password', 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=False, can_post_password=True)
self._test_execute(is_password_set=True, can_post_password=True, self._test_execute(is_password_set=True, can_post_password=True,
can_update_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)

View File

@ -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)