diff --git a/cloudbaseinit/conf/default.py b/cloudbaseinit/conf/default.py index 4218f3f3..bd75c026 100644 --- a/cloudbaseinit/conf/default.py +++ b/cloudbaseinit/conf/default.py @@ -86,6 +86,12 @@ class GlobalOptions(conf_base.Options): 'winrm_enable_basic_auth', default=True, help='Enables basic authentication for the WinRM ' 'HTTPS listener'), + cfg.BoolOpt( + 'winrm_configure_http_listener', default=False, + help='Configures the WinRM HTTP listener'), + cfg.BoolOpt( + 'winrm_configure_https_listener', default=True, + help='Configures the WinRM HTTPS listener'), cfg.ListOpt( 'volumes_to_extend', default=None, help='List of volumes that need to be extended ' diff --git a/cloudbaseinit/metadata/services/base.py b/cloudbaseinit/metadata/services/base.py index a3d315c3..861acff7 100644 --- a/cloudbaseinit/metadata/services/base.py +++ b/cloudbaseinit/metadata/services/base.py @@ -154,6 +154,9 @@ class BaseMetadataService(object): def post_password(self, enc_password_b64): pass + def get_winrm_listeners_configuration(self): + pass + def get_client_auth_certs(self): pass diff --git a/cloudbaseinit/plugins/windows/winrmlistener.py b/cloudbaseinit/plugins/windows/winrmlistener.py index 28da41c9..34ee337f 100644 --- a/cloudbaseinit/plugins/windows/winrmlistener.py +++ b/cloudbaseinit/plugins/windows/winrmlistener.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib + from oslo_log import log as oslo_logging from cloudbaseinit import conf as cloudbaseinit_conf @@ -50,54 +52,95 @@ class ConfigWinRMListenerPlugin(base.BasePlugin): return True - def execute(self, service, shared_data): - osutils = osutils_factory.get_os_utils() + @contextlib.contextmanager + def _check_uac_remote_restrictions(self, osutils): security_utils = security.WindowsSecurityUtils() - - if not self._check_winrm_service(osutils): - return base.PLUGIN_EXECUTE_ON_NEXT_BOOT, False - # On Windows Vista, 2008, 2008 R2 and 7, changing the configuration of # the winrm service will fail with an "Access is denied" error if the # User Account Control remote restrictions are enabled. # The solution to this issue is to temporarily disable the User Account # Control remote restrictions. # https://support.microsoft.com/kb/951016 - disable_uac_remote_restrictions = (osutils.check_os_version(6, 0) and - not osutils.check_os_version(6, 2) - and security_utils - .get_uac_remote_restrictions()) - + disable_uac_remote_restrictions = ( + osutils.check_os_version(6, 0) and + not osutils.check_os_version(6, 2) and + security_utils.get_uac_remote_restrictions()) try: if disable_uac_remote_restrictions: LOG.debug("Disabling UAC remote restrictions") security_utils.set_uac_remote_restrictions(enable=False) - - winrm_config = winrmconfig.WinRMConfig() - winrm_config.set_auth_config(basic=CONF.winrm_enable_basic_auth) - - cert_manager = x509.CryptoAPICertManager() - cert_thumbprint = cert_manager.create_self_signed_cert( - self._cert_subject) - - protocol = winrmconfig.LISTENER_PROTOCOL_HTTPS - - if winrm_config.get_listener(protocol=protocol): - winrm_config.delete_listener(protocol=protocol) - - winrm_config.create_listener(cert_thumbprint=cert_thumbprint, - protocol=protocol) - - listener_config = winrm_config.get_listener(protocol=protocol) - listener_port = listener_config.get("Port") - - rule_name = "WinRM %s" % protocol - osutils.firewall_create_rule(rule_name, listener_port, - osutils.PROTOCOL_TCP) - + yield finally: if disable_uac_remote_restrictions: LOG.debug("Enabling UAC remote restrictions") security_utils.set_uac_remote_restrictions(enable=True) + def _configure_winrm_listener(self, osutils, winrm_config, protocol, + cert_thumbprint=None): + if winrm_config.get_listener(protocol=protocol): + winrm_config.delete_listener(protocol=protocol) + + winrm_config.create_listener(cert_thumbprint=cert_thumbprint, + protocol=protocol) + + listener_config = winrm_config.get_listener(protocol=protocol) + listener_port = listener_config.get("Port") + + rule_name = "WinRM %s" % protocol + osutils.firewall_create_rule(rule_name, listener_port, + osutils.PROTOCOL_TCP) + + def _get_winrm_listeners_config(self, service): + listeners_config = service.get_winrm_listeners_configuration() + if listeners_config is None: + listeners_config = [] + if CONF.winrm_configure_http_listener: + listeners_config.append( + {"protocol": winrmconfig.LISTENER_PROTOCOL_HTTP}) + if CONF.winrm_configure_https_listener: + listeners_config.append( + {"protocol": winrmconfig.LISTENER_PROTOCOL_HTTPS}) + return listeners_config + + def _create_self_signed_certificate(self): + LOG.info("Generating self signed certificate for WinRM HTTPS listener") + cert_manager = x509.CryptoAPICertManager() + cert_thumbprint = cert_manager.create_self_signed_cert( + self._cert_subject) + return cert_thumbprint + + def execute(self, service, shared_data): + osutils = osutils_factory.get_os_utils() + + if not self._check_winrm_service(osutils): + return base.PLUGIN_EXECUTE_ON_NEXT_BOOT, False + + listeners_config = self._get_winrm_listeners_config(service) + + if not listeners_config: + LOG.info("No WinRM listener configuration provided") + else: + with self._check_uac_remote_restrictions(osutils): + winrm_config = winrmconfig.WinRMConfig() + winrm_config.set_auth_config( + basic=CONF.winrm_enable_basic_auth) + + for listener_config in listeners_config: + protocol = listener_config["protocol"].upper() + + cert_thumb = None + if protocol == winrmconfig.LISTENER_PROTOCOL_HTTPS: + cert_thumb = listener_config.get( + "certificate_thumbprint") + if not cert_thumb: + cert_thumb = self._create_self_signed_certificate() + + LOG.info("Configuring WinRM listener for protocol: " + "%(protocol)s, certificate thumbprint: " + "%(cert_thumb)s", + {"protocol": protocol, + "cert_thumb": cert_thumb}) + self._configure_winrm_listener( + osutils, winrm_config, protocol, cert_thumb) + return base.PLUGIN_EXECUTION_DONE, False diff --git a/cloudbaseinit/tests/plugins/windows/test_winrmlistener.py b/cloudbaseinit/tests/plugins/windows/test_winrmlistener.py index 9f1badb1..bb70691f 100644 --- a/cloudbaseinit/tests/plugins/windows/test_winrmlistener.py +++ b/cloudbaseinit/tests/plugins/windows/test_winrmlistener.py @@ -41,7 +41,8 @@ class ConfigWinRMListenerPluginTests(unittest.TestCase): 'ctypes.wintypes': self._mock_wintypes, 'pywintypes': self._mock_pywintypes, 'win32com': self._mock_win32, - 'six.moves': self._moves_mock}) + 'six.moves': self._moves_mock + }) self._module_patcher.start() self._winreg_mock = self._moves_mock.winreg @@ -93,69 +94,191 @@ class ConfigWinRMListenerPluginTests(unittest.TestCase): def test_check_winrm_service_no_service(self): self._test_check_winrm_service(service_exists=False) + @mock.patch('cloudbaseinit.utils.windows.security.' + 'WindowsSecurityUtils') + def _test_check_uac_remote_restrictions(self, mock_SecurityUtils, + disable_uac_remote_restrictions): + mock_security_utils = mock.MagicMock() + mock_SecurityUtils.return_value = mock_security_utils + mock_osutils = mock.Mock() + mock_osutils.check_os_version.side_effect = [True, False] + if disable_uac_remote_restrictions: + mock_security_utils.get_uac_remote_restrictions.return_value = \ + disable_uac_remote_restrictions + + with self._winrmlistener._check_uac_remote_restrictions(mock_osutils): + mock_SecurityUtils.assert_called_once_with() + mock_osutils.check_os_version.assert_has_calls( + [mock.call(6, 0), mock.call(6, 2)]) + (mock_security_utils.get_uac_remote_restrictions. + assert_called_once_with()) + if disable_uac_remote_restrictions: + expected_set_token_calls = [mock.call(enable=True)] + else: + expected_set_token_calls = [mock.call(enable=False), + mock.call(enable=True)] + mock_security_utils.set_uac_remote_restrictions.has_calls( + expected_set_token_calls) + + def test_check_uac_remote_restrictions(self): + self._test_check_uac_remote_restrictions( + disable_uac_remote_restrictions=True) + + def test_check_uac_remote_restrictions_no_disable_restrictions(self): + self._test_check_uac_remote_restrictions( + disable_uac_remote_restrictions=False) + + def _test_configure_winrm_listener(self, has_listener=True): + mock_listener_config = mock.MagicMock() + mock_winrm_config = mock.MagicMock() + mock_osutils = mock.MagicMock() + mock_osutils.PROTOCOL_TCP = mock.sentinel.PROTOCOL_TCP + mock_winrm_config.get_listener.side_effect = [ + has_listener, mock_listener_config] + port = 9999 + protocol = mock.sentinel.protocol + cert_thumbprint = mock.sentinel.cert_thumbprint + mock_listener_config.get.return_value = port + + self._winrmlistener._configure_winrm_listener( + mock_osutils, mock_winrm_config, protocol, cert_thumbprint) + + if has_listener: + mock_winrm_config.delete_listener.assert_called_once_with( + protocol=protocol) + mock_winrm_config.create_listener.assert_called_once_with( + cert_thumbprint=cert_thumbprint, protocol=protocol) + mock_listener_config.get.assert_called_once_with("Port") + mock_osutils.firewall_create_rule.assert_called_once_with( + "WinRM %s" % protocol, port, mock_osutils.PROTOCOL_TCP) + + def test_configure_winrm_listener(self): + self._test_configure_winrm_listener() + + def test_configure_winrm_listener_no_initial_listener(self): + self._test_configure_winrm_listener(has_listener=False) + + def _test_get_winrm_listeners_config(self, listeners_config=None, + http_listener=None, + https_listener=None): + winrmconfig = importlib.import_module('cloudbaseinit.utils.' + 'windows.winrmconfig') + mock_service = mock.MagicMock() + mock_service.get_winrm_listeners_configuration.return_value = \ + listeners_config + expected_result = listeners_config + if listeners_config is None: + expected_result = [] + if http_listener: + expected_result.append( + {"protocol": winrmconfig.LISTENER_PROTOCOL_HTTP}) + if https_listener: + expected_result.append( + {"protocol": winrmconfig.LISTENER_PROTOCOL_HTTPS}) + + with testutils.ConfPatcher("winrm_configure_http_listener", + http_listener): + with testutils.ConfPatcher("winrm_configure_https_listener", + https_listener): + result = self._winrmlistener._get_winrm_listeners_config( + mock_service) + + self.assertEqual(result, expected_result) + + def test_get_winrm_listeners_config_has_listeners(self): + self._test_get_winrm_listeners_config( + listeners_config=mock.sentinel.listeners) + + def test_get_winrm_listeners_config_http_listener(self): + self._test_get_winrm_listeners_config(http_listener=True) + + def test_get_winrm_listeners_config_https_listener(self): + self._test_get_winrm_listeners_config(https_listener=True) + + @mock.patch('cloudbaseinit.utils.windows.x509.CryptoAPICertManager') + def test_create_self_signed_certificate(self, mock_CryptoAPICertManager): + mock_cert_mgr = mock.MagicMock() + mock_CryptoAPICertManager.return_value = mock_cert_mgr + mock_cert_mgr.create_self_signed_cert.return_value = \ + mock.sentinel.cert_thumbprint + result = self._winrmlistener._create_self_signed_certificate() + self.assertEqual(result, mock.sentinel.cert_thumbprint) + mock_CryptoAPICertManager.assert_called_once_with() + mock_cert_mgr.create_self_signed_cert.assert_called_once_with( + self._winrmlistener._cert_subject) + + @mock.patch('cloudbaseinit.plugins.windows.winrmlistener.' + 'ConfigWinRMListenerPlugin._configure_winrm_listener') + @mock.patch('cloudbaseinit.plugins.windows.winrmlistener.' + 'ConfigWinRMListenerPlugin._check_uac_remote_restrictions') + @mock.patch('cloudbaseinit.plugins.windows.winrmlistener.' + 'ConfigWinRMListenerPlugin._get_winrm_listeners_config') @mock.patch('cloudbaseinit.osutils.factory.get_os_utils') @mock.patch('cloudbaseinit.plugins.windows.winrmlistener.' 'ConfigWinRMListenerPlugin._check_winrm_service') @mock.patch('cloudbaseinit.utils.windows.winrmconfig.WinRMConfig') - @mock.patch('cloudbaseinit.utils.windows.x509.CryptoAPICertManager' - '.create_self_signed_cert') - @mock.patch('cloudbaseinit.utils.windows.security.WindowsSecurityUtils' - '.set_uac_remote_restrictions') - @mock.patch('cloudbaseinit.utils.windows.security.WindowsSecurityUtils' - '.get_uac_remote_restrictions') - def _test_execute(self, get_uac_rs, set_uac_rs, mock_create_cert, - mock_WinRMConfig, + @mock.patch('cloudbaseinit.plugins.windows.winrmlistener' + '.ConfigWinRMListenerPlugin._create_self_signed_certificate') + def _test_execute(self, mock_create_cert, mock_WinRMConfig, mock_check_winrm_service, mock_get_os_utils, - service_status): - mock_service = mock.MagicMock() - mock_listener_config = mock.MagicMock() - mock_cert_thumbprint = mock.MagicMock() - shared_data = 'fake data' + mock_get_winrm_listeners, mock_check_restrictions, + mock_configure_listener, + service_status=True, protocol=None, + listeners_config=True, certificate_thumbprint=None): + mock_winrm_config = mock.MagicMock() + mock_WinRMConfig.return_value = mock_winrm_config mock_osutils = mock.MagicMock() mock_get_os_utils.return_value = mock_osutils mock_check_winrm_service.return_value = service_status - mock_create_cert.return_value = mock_cert_thumbprint - mock_WinRMConfig().get_listener.return_value = mock_listener_config - mock_listener_config.get.return_value = 9999 - - mock_osutils.check_os_version.side_effect = [True, False] - get_uac_rs.return_value = True - - expected_check_version_calls = [mock.call(6, 0), mock.call(6, 2)] - expected_set_token_calls = [mock.call(enable=False), - mock.call(enable=True)] - - response = self._winrmlistener.execute(mock_service, shared_data) + if not service_status: + expected_result = (base.PLUGIN_EXECUTE_ON_NEXT_BOOT, False) + elif not listeners_config: + mock_get_winrm_listeners.return_value = None + expected_result = (base.PLUGIN_EXECUTION_DONE, False) + else: + expected_result = (base.PLUGIN_EXECUTION_DONE, False) + if certificate_thumbprint is not None: + certificate_thumbprint = \ + str(mock.sentinel.certificate_thumbprint) + listener_config = { + "protocol": protocol, + "certificate_thumbprint": certificate_thumbprint + } + mock_get_winrm_listeners.return_value = [listener_config] + with testutils.ConfPatcher('winrm_enable_basic_auth', + str(mock.sentinel.winrm_enable_basic_auth)): + result = self._winrmlistener.execute( + mock.sentinel.service, mock.sentinel.shared_data) + self.assertEqual(result, expected_result) mock_get_os_utils.assert_called_once_with() mock_check_winrm_service.assert_called_once_with(mock_osutils) - - if not service_status: - self.assertEqual((base.PLUGIN_EXECUTE_ON_NEXT_BOOT, - service_status), response) - else: - self.assertEqual(expected_check_version_calls, - mock_osutils.check_os_version.call_args_list) - self.assertEqual(expected_set_token_calls, - set_uac_rs.call_args_list) - mock_WinRMConfig().set_auth_config.assert_called_once_with( - basic=CONF.winrm_enable_basic_auth) - mock_create_cert.assert_called_once_with( - self._winrmlistener._cert_subject) - - mock_WinRMConfig().get_listener.assert_called_with( - protocol="HTTPS") - mock_WinRMConfig().delete_listener.assert_called_once_with( - protocol="HTTPS") - mock_WinRMConfig().create_listener.assert_called_once_with( - protocol="HTTPS", cert_thumbprint=mock_cert_thumbprint) - mock_listener_config.get.assert_called_once_with("Port") - mock_osutils.firewall_create_rule.assert_called_once_with( - "WinRM HTTPS", 9999, mock_osutils.PROTOCOL_TCP) - self.assertEqual((base.PLUGIN_EXECUTION_DONE, False), response) - - def test_execute(self): - self._test_execute(service_status=True) + if service_status: + mock_get_winrm_listeners.assert_called_once_with( + mock.sentinel.service) + if listeners_config: + mock_check_restrictions.assert_called_once_with(mock_osutils) + mock_WinRMConfig.assert_called_once_with() + mock_winrm_config.set_auth_config.assert_called_once_with( + basic=str(mock.sentinel.winrm_enable_basic_auth)) + winrmconfig = importlib.import_module('cloudbaseinit.utils.' + 'windows.winrmconfig') + if (protocol == winrmconfig.LISTENER_PROTOCOL_HTTPS and + not certificate_thumbprint): + certificate_thumbprint = mock_create_cert.return_value + mock_create_cert.assert_called_once_with() + mock_configure_listener.assert_called_once_with( + mock_osutils, mock_winrm_config, protocol.upper(), + certificate_thumbprint) def test_execute_service_status_is_false(self): self._test_execute(service_status=False) + + def test_execute_no_listeners_config(self): + self._test_execute(listeners_config=None) + + def test_execute_http_protocol(self): + self._test_execute(protocol=str(mock.sentinel.http)) + + def test_execute_https_protocol(self): + self._test_execute(protocol="HTTPS") diff --git a/cloudbaseinit/tests/utils/windows/test_winrmconfig.py b/cloudbaseinit/tests/utils/windows/test_winrmconfig.py index 6592b160..4ce0d095 100644 --- a/cloudbaseinit/tests/utils/windows/test_winrmconfig.py +++ b/cloudbaseinit/tests/utils/windows/test_winrmconfig.py @@ -342,7 +342,7 @@ class WinRMConfigTests(unittest.TestCase): '' 'wsman' '' % {"enabled": True, - "cert_thumbprint": None}) + "cert_thumbprint": ""}) @mock.patch('xml.etree.ElementTree.fromstring') @mock.patch('xml.etree.ElementTree.tostring') diff --git a/cloudbaseinit/utils/windows/winrmconfig.py b/cloudbaseinit/utils/windows/winrmconfig.py index 8fb6a058..fe7e8410 100644 --- a/cloudbaseinit/utils/windows/winrmconfig.py +++ b/cloudbaseinit/utils/windows/winrmconfig.py @@ -164,7 +164,7 @@ class WinRMConfig(object): '' 'wsman' '' % {"enabled": self._get_xml_bool(enabled), - "cert_thumbprint": cert_thumbprint}) + "cert_thumbprint": cert_thumbprint or ""}) def set_auth_config(self, basic=None, kerberos=None, negotiate=None, certificate=None, credSSP=None,