Adds WinRM certificate auth plugin
This commit is contained in:
parent
024758ff10
commit
b5030fa9d8
88
cloudbaseinit/plugins/windows/winrmcertificateauth.py
Normal file
88
cloudbaseinit/plugins/windows/winrmcertificateauth.py
Normal file
@ -0,0 +1,88 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 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 cloudbaseinit.openstack.common import log as logging
|
||||
from cloudbaseinit.plugins import base
|
||||
from cloudbaseinit.plugins import constants
|
||||
from cloudbaseinit.plugins.windows import x509
|
||||
from cloudbaseinit.plugins.windows import winrmconfig
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigWinRMCertificateAuthPlugin(base.BasePlugin):
|
||||
def _get_client_auth_cert(self, service):
|
||||
meta_data = service.get_meta_data('openstack')
|
||||
meta = meta_data.get('meta')
|
||||
if meta:
|
||||
i = 0
|
||||
cert_data = ""
|
||||
while True:
|
||||
# Chunking is necessary as metadata items can be
|
||||
# max. 255 chars long
|
||||
cert_chunk = meta.get('admin_cert%d' % i)
|
||||
if not cert_chunk:
|
||||
break
|
||||
cert_data += cert_chunk
|
||||
i += 1
|
||||
|
||||
return cert_data
|
||||
|
||||
def _get_credentials(self, shared_data):
|
||||
user_name = shared_data.get(constants.SHARED_DATA_USERNAME)
|
||||
if not user_name:
|
||||
raise Exception("Cannot execute plugin as the username has "
|
||||
"not been set in the plugins shared data")
|
||||
|
||||
password = shared_data.get(constants.SHARED_DATA_PASSWORD)
|
||||
if not password:
|
||||
raise Exception("Cannot execute plugin as the password has "
|
||||
"not been set in the plugins shared data")
|
||||
|
||||
# For security reasons unset the password in the shared_data
|
||||
# as it is currently not needed by other plugins
|
||||
shared_data[constants.SHARED_DATA_PASSWORD] = None
|
||||
|
||||
return (user_name, password)
|
||||
|
||||
def execute(self, service, shared_data):
|
||||
user_name, password = self._get_credentials(shared_data)
|
||||
|
||||
cert_data = self._get_client_auth_cert(service)
|
||||
if not cert_data:
|
||||
LOG.info("WinRM certificate authentication cannot be configured "
|
||||
"as a certificate has not been provided in the metadata")
|
||||
return (base.PLUGIN_EXECUTION_DONE, False)
|
||||
|
||||
cert_manager = x509.CryptoAPICertManager()
|
||||
cert_thumprint, cert_upn = cert_manager.import_cert(
|
||||
cert_data, store_name=x509.STORE_NAME_ROOT)
|
||||
|
||||
if not cert_upn:
|
||||
LOG.error("WinRM certificate authentication cannot be configured "
|
||||
"as the provided certificate lacks a subject alt name "
|
||||
"containing an UPN (OID 1.3.6.1.4.1.311.20.2.3)")
|
||||
return (base.PLUGIN_EXECUTION_DONE, False)
|
||||
|
||||
winrm_config = winrmconfig.WinRMConfig()
|
||||
winrm_config.set_auth_config(certificate=True)
|
||||
|
||||
if winrm_config.get_cert_mapping(cert_thumprint, cert_upn):
|
||||
winrm_config.delete_cert_mapping(cert_thumprint, cert_upn)
|
||||
winrm_config.create_cert_mapping(cert_thumprint, cert_upn,
|
||||
user_name, password)
|
||||
|
||||
return (base.PLUGIN_EXECUTION_DONE, False)
|
@ -31,7 +31,10 @@ LISTENER_PROTOCOL_HTTPS = "HTTPS"
|
||||
|
||||
class WinRMConfig(object):
|
||||
_SERVICE_AUTH_URI = 'winrm/Config/Service/Auth'
|
||||
_SERVICE_LISTENER_URI = 'winrm/Config/Listener?Address=*+Transport=%s'
|
||||
_SERVICE_LISTENER_URI = ('winrm/Config/Listener?Address='
|
||||
'%(address)s+Transport=%(protocol)s')
|
||||
_SERVICE_CERTMAPPING_URI = ('winrm/Config/Service/certmapping?Issuer='
|
||||
'%(issuer)s+Subject=%(subject)s+Uri=%(uri)s')
|
||||
|
||||
def _get_wsman_session(self):
|
||||
wsman = client.Dispatch('WSMan.Automation')
|
||||
@ -41,6 +44,9 @@ class WinRMConfig(object):
|
||||
return re.match("^{.*}(.*)$", tag).groups(1)[0]
|
||||
|
||||
def _parse_listener_xml(self, data_xml):
|
||||
if not data_xml:
|
||||
return None
|
||||
|
||||
listening_on = []
|
||||
data = {"ListeningOn": listening_on}
|
||||
|
||||
@ -64,50 +70,110 @@ class WinRMConfig(object):
|
||||
|
||||
return data
|
||||
|
||||
def get_listener(self, protocol=LISTENER_PROTOCOL_HTTPS):
|
||||
def _parse_cert_mapping_xml(self, data_xml):
|
||||
if not data_xml:
|
||||
return None
|
||||
|
||||
data = {}
|
||||
|
||||
ns = {'cfg':
|
||||
'http://schemas.microsoft.com/wbem/wsman/1/config/service/'
|
||||
'certmapping.xsd'}
|
||||
tree = ElementTree.fromstring(data_xml)
|
||||
for node in tree:
|
||||
tag = self._get_node_tag(node.tag)
|
||||
if tag == "Enabled":
|
||||
if node.text == "true":
|
||||
value = True
|
||||
else:
|
||||
value = False
|
||||
data[tag] = value
|
||||
else:
|
||||
data[tag] = node.text
|
||||
|
||||
return data
|
||||
|
||||
def _get_xml_bool(self, value):
|
||||
if value:
|
||||
return "true"
|
||||
else:
|
||||
return "false"
|
||||
|
||||
def _get_resource(self, resource_uri):
|
||||
session = self._get_wsman_session()
|
||||
resourceUri = self._SERVICE_LISTENER_URI % protocol
|
||||
try:
|
||||
data_xml = session.Get(resourceUri)
|
||||
return session.Get(resource_uri)
|
||||
except pywintypes.com_error, ex:
|
||||
if len(ex.excepinfo) > 5 and ex.excepinfo[5] == -2144108544:
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
|
||||
return self._parse_listener_xml(data_xml)
|
||||
|
||||
def delete_listener(self, protocol=LISTENER_PROTOCOL_HTTPS):
|
||||
def _delete_resource(self, resource_uri):
|
||||
session = self._get_wsman_session()
|
||||
resourceUri = self._SERVICE_LISTENER_URI % protocol
|
||||
session.Delete(resourceUri)
|
||||
session.Delete(resource_uri)
|
||||
|
||||
def create_listener(self, protocol=LISTENER_PROTOCOL_HTTPS, enabled=True,
|
||||
cert_thumbprint=None):
|
||||
def _create_resource(self, resource_uri, data_xml):
|
||||
session = self._get_wsman_session()
|
||||
resource_uri = self._SERVICE_LISTENER_URI % protocol
|
||||
session.Create(resource_uri, data_xml)
|
||||
|
||||
if enabled:
|
||||
enabled_str = "true"
|
||||
else:
|
||||
enabled_str = "false"
|
||||
def get_cert_mapping(self, issuer, subject, uri="*"):
|
||||
resource_uri = self._SERVICE_CERTMAPPING_URI % {'issuer': issuer,
|
||||
'subject': subject,
|
||||
'uri': uri}
|
||||
return self._parse_cert_mapping_xml(self._get_resource(resource_uri))
|
||||
|
||||
session.Create(
|
||||
def delete_cert_mapping(self, issuer, subject, uri="*"):
|
||||
resource_uri = self._SERVICE_CERTMAPPING_URI % {'issuer': issuer,
|
||||
'subject': subject,
|
||||
'uri': uri}
|
||||
self._delete_resource(resource_uri)
|
||||
|
||||
def create_cert_mapping(self, issuer, subject, username, password,
|
||||
uri="*", enabled=True):
|
||||
resource_uri = self._SERVICE_CERTMAPPING_URI % {'issuer': issuer,
|
||||
'subject': subject,
|
||||
'uri': uri}
|
||||
self._create_resource(
|
||||
resource_uri,
|
||||
'<p:certmapping xmlns:p="http://schemas.microsoft.com/wbem/wsman/'
|
||||
'1/config/service/certmapping.xsd">'
|
||||
'<p:Enabled>%(enabled)s</p:Enabled>'
|
||||
'<p:Password>%(password)s</p:Password>'
|
||||
'<p:UserName>%(username)s</p:UserName>'
|
||||
'</p:certmapping>' % {'enabled': self._get_xml_bool(enabled),
|
||||
'username': username,
|
||||
'password': password})
|
||||
|
||||
def get_listener(self, protocol=LISTENER_PROTOCOL_HTTPS, address="*"):
|
||||
resource_uri = self._SERVICE_LISTENER_URI % {'protocol': protocol,
|
||||
'address': address}
|
||||
return self._parse_listener_xml(self._get_resource(resource_uri))
|
||||
|
||||
def delete_listener(self, protocol=LISTENER_PROTOCOL_HTTPS, address="*"):
|
||||
resource_uri = self._SERVICE_LISTENER_URI % {'protocol': protocol,
|
||||
'address': address}
|
||||
self._delete_resource(resource_uri)
|
||||
|
||||
def create_listener(self, protocol=LISTENER_PROTOCOL_HTTPS,
|
||||
cert_thumbprint=None, address="*", enabled=True):
|
||||
resource_uri = self._SERVICE_LISTENER_URI % {'protocol': protocol,
|
||||
'address': address}
|
||||
self._create_resource(
|
||||
resource_uri,
|
||||
'<p:Listener xmlns:p="http://schemas.microsoft.com/'
|
||||
'wbem/wsman/1/config/listener.xsd">'
|
||||
'<p:Enabled>%(enabled_str)s</p:Enabled>'
|
||||
'<p:Enabled>%(enabled)s</p:Enabled>'
|
||||
'<p:CertificateThumbPrint>%(cert_thumbprint)s'
|
||||
'</p:CertificateThumbPrint>'
|
||||
'<p:URLPrefix>wsman</p:URLPrefix>'
|
||||
'</p:Listener>' % {"enabled_str": enabled_str,
|
||||
'</p:Listener>' % {"enabled": self._get_xml_bool(enabled),
|
||||
"cert_thumbprint": cert_thumbprint})
|
||||
|
||||
def get_auth_config(self):
|
||||
data = {}
|
||||
|
||||
session = self._get_wsman_session()
|
||||
data_xml = session.Get(self._SERVICE_AUTH_URI)
|
||||
data_xml = self._get_resource(self._SERVICE_AUTH_URI)
|
||||
tree = ElementTree.fromstring(data_xml)
|
||||
for node in tree:
|
||||
tag = self._get_node_tag(node.tag)
|
||||
@ -142,13 +208,9 @@ class WinRMConfig(object):
|
||||
|
||||
for (tag, value) in tag_map.items():
|
||||
if value is not None:
|
||||
if value:
|
||||
new_value = "true"
|
||||
else:
|
||||
new_value = "false"
|
||||
|
||||
node = tree.find('.//cfg:%s' % tag, namespaces=ns)
|
||||
|
||||
new_value = self._get_xml_bool(value)
|
||||
if node.text.lower() != new_value:
|
||||
node.text = new_value
|
||||
data_xml = ElementTree.tostring(tree)
|
||||
|
@ -30,6 +30,10 @@ free = ctypes.cdll.msvcrt.free
|
||||
free.restype = None
|
||||
free.argtypes = [ctypes.c_void_p]
|
||||
|
||||
STORE_NAME_MY = "My"
|
||||
STORE_NAME_ROOT = "Root"
|
||||
STORE_NAME_TRUSTED_PEOPLE = "TrustedPeople"
|
||||
|
||||
|
||||
class CryptoAPICertManager(object):
|
||||
def _get_cert_thumprint(self, cert_context_p):
|
||||
@ -101,7 +105,7 @@ class CryptoAPICertManager(object):
|
||||
cryptoapi.CryptReleaseContext(crypt_prov_handle, 0)
|
||||
|
||||
def create_self_signed_cert(self, subject, validity_years=10,
|
||||
machine_keyset=True, store_name="MY"):
|
||||
machine_keyset=True, store_name=STORE_NAME_MY):
|
||||
subject_encoded = None
|
||||
cert_context_p = None
|
||||
store_handle = None
|
||||
@ -208,7 +212,7 @@ class CryptoAPICertManager(object):
|
||||
return base64_cert_data.replace("\n", "")
|
||||
|
||||
def import_cert(self, cert_data, machine_keyset=True,
|
||||
store_name="TrustedPeople"):
|
||||
store_name=STORE_NAME_MY):
|
||||
|
||||
base64_cert_data = self._get_cert_base64(cert_data)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user