Added support for post metadata encrypted password

This commit is contained in:
Alessandro Pilotti 2013-02-03 22:00:04 +02:00
parent 11e183212f
commit e94d732d2b
5 changed files with 321 additions and 29 deletions

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import abc
import json
import os
import posixpath
@ -23,16 +24,21 @@ from cloudbaseinit.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class NotExistingMetadataException(Exception):
pass
class BaseMetadataService(object):
def load(self):
self._cache = {}
@abc.abstractmethod
def _get_data(self, path):
pass
def _get_cache_data(self, path):
if path in self._cache:
LOG.debug('Using cached copy of metadata: \'%s\'' % path)
LOG.debug("Using cached copy of metadata: '%s'" % path)
return self._cache[path]
else:
data = self._get_data(path)
@ -57,5 +63,16 @@ class BaseMetadataService(object):
return json.loads(self._get_cache_data(path))
else:
return data
def _post_data(self, path, data):
raise NotImplementedError()
def post_password(self, enc_password_b64, version='latest'):
path = posixpath.normpath(posixpath.join(path,
'openstack',
version,
'password'))
return self._post_data(path, enc_password_b64)
def cleanup(self):
pass

View File

@ -15,6 +15,7 @@
# under the License.
import posixpath
import urllib
import urllib2
from cloudbaseinit.metadata.services.base import *
@ -42,9 +43,39 @@ class HttpService(BaseMetadataService):
CONF.metadata_base_url)
return False
def _get_response(self, req):
try:
return urllib2.urlopen(req)
except urllib2.HTTPError as ex:
if ex.code == 404:
raise NotExistingMetadataException()
else:
raise ex
def _get_data(self, path):
norm_path = posixpath.join(CONF.metadata_base_url, path)
LOG.debug('Getting metadata from: %(norm_path)s' % locals())
req = urllib2.Request(norm_path)
response = urllib2.urlopen(req)
response = self._get_response(req)
return response.read()
def _post_data(self, path, data):
norm_path = posixpath.join(CONF.metadata_base_url, path)
LOG.debug('Posting metadata to: %(norm_path)s' % locals())
req = urllib2.Request(norm_path, data=urllib.urlencode(data))
self._get_response(req)
return True
def post_password(self, enc_password_b64, version='latest'):
try:
return super(HttpService, self).post_password(enc_password_b64,
version)
except urllib2.HTTPError as ex:
if ex.code == 409:
# Password already set
return False
else:
raise ex

View File

@ -2,24 +2,27 @@
# Copyright 2012 Cloudbase Solutions Srl
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# 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
# 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 uuid
import base64
import os
from cloudbaseinit.metadata.services import base as services_base
from cloudbaseinit.openstack.common import cfg
from cloudbaseinit.openstack.common import log as logging
from cloudbaseinit.osutils.factory import *
from cloudbaseinit.plugins.base import *
from cloudbaseinit.osutils import factory as osutils_factory
from cloudbaseinit.plugins import base
from cloudbaseinit.utils import crypt
opts = [
cfg.StrOpt('username', default='Admin',
@ -38,40 +41,91 @@ CONF.register_opts(opts)
LOG = logging.getLogger(__name__)
class CreateUserPlugin(BasePlugin):
def execute(self, service):
username = CONF.username
class CreateUserPlugin(base.BasePlugin):
_metadata_version = '2013-04-04'
def _generate_random_password(self, length):
# On Windows os.urandom() uses CryptGenRandom, which is a
# cryptographically secure pseudorandom number generator
b64_password = base64.b64encode(os.urandom(256))
return b64_password.replace('/', '').replace('+', '')[:length]
def _encrypt_password(self, ssh_pub_key, password):
cm = crypt.CryptManager()
with cm.load_ssh_rsa_public_key(ssh_pub_key) as rsa:
enc_password = rsa.public_encrypt(password)
return base64.b64encode(enc_password)
def _get_ssh_public_key(self, service):
meta_data = service.get_meta_data('openstack', self._metadata_version)
if not 'public_keys' in meta_data:
return False
public_keys = meta_data['public_keys']
ssh_pub_key = None
for k in public_keys:
# Get the first key
ssh_pub_key = public_keys[k]
break
return ssh_pub_key
def _get_password(self, service):
meta_data = service.get_meta_data('openstack')
if 'admin_pass' in meta_data and CONF.inject_user_password:
password = meta_data['admin_pass']
else:
password = None
# Generate a random password
# Limit to 14 chars for compatibility with NT
password = self._generate_random_password(14)
return password
osutils = OSUtilsFactory().get_os_utils()
if not osutils.user_exists(username):
if not password:
# Generate a random password
# Limit to 14 chars for compatibility with NT
LOG.debug("Generating a random password")
password = str(uuid.uuid4()).replace('-', '')[:14]
def _set_metadata_password(self, password, service):
try:
ssh_pub_key = self._get_ssh_public_key(service)
if ssh_pub_key:
enc_password_b64 = self._encrypt_password(ssh_pub_key,
password)
return service.post_password(enc_password_b64,
self._metadata_version)
else:
LOG.info('No SSH public key available for password encryption')
return True
except services_base.NotExistingMetadataException:
# Requested version not available or password feature
# not implemented
return True
osutils.create_user(username, password)
else:
if password:
osutils.set_user_password(username, password)
def execute(self, service):
user_name = CONF.username
if password:
password = self._get_password(service)
md_pwd_already_set = not self._set_metadata_password(password,
service)
osutils = osutils_factory.OSUtilsFactory().get_os_utils()
if not osutils.user_exists(user_name):
if md_pwd_already_set:
LOG.warning('Creating user, but the password was not set in '
'the metadata as it was previously set')
osutils.create_user(user_name, password)
# Create a user profile in order for other plugins
# to access the user home, etc
token = osutils.create_user_logon_session(username, password, True)
token = osutils.create_user_logon_session(user_name,
password,
True)
osutils.close_user_logon_session(token)
else:
if not md_pwd_already_set:
osutils.set_user_password(user_name, password)
else:
LOG.warning('Cannot change the user\'s password as it is '
'already set in the metadata')
for group in CONF.groups:
for group_name in CONF.groups:
try:
osutils.add_user_to_local_group(username, group)
except Exception, ex:
LOG.error('Cannot add user to group \'%(group)s\'. '
'Error: %(ex)s' % locals())
osutils.add_user_to_local_group(user_name, group_name)
except Exception as ex:
LOG.exception(ex)
LOG.error('Cannot add user to group "%s"' % group_name)
return False

View File

View File

@ -0,0 +1,190 @@
# 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.
import base64
import ctypes
import ctypes.util
import struct
import sys
if sys.platform == "win32":
openssl_lib_name = "libeay32"
else:
openssl_lib_name = "ssl"
openssl = ctypes.CDLL(ctypes.util.find_library(openssl_lib_name))
clib = ctypes.CDLL(ctypes.util.find_library("c"))
class RSA(ctypes.Structure):
_fields_ = [
("pad", ctypes.c_int),
("version", ctypes.c_long),
("meth", ctypes.c_void_p),
("engine", ctypes.c_void_p),
("n", ctypes.c_void_p),
("e", ctypes.c_void_p),
("d", ctypes.c_void_p),
("p", ctypes.c_void_p),
("q", ctypes.c_void_p),
("dmp1", ctypes.c_void_p),
("dmq1", ctypes.c_void_p),
("iqmp", ctypes.c_void_p),
("sk", ctypes.c_void_p),
("dummy", ctypes.c_int),
("references", ctypes.c_int),
("flags", ctypes.c_int),
("_method_mod_n", ctypes.c_void_p),
("_method_mod_p", ctypes.c_void_p),
("_method_mod_q", ctypes.c_void_p),
("bignum_data", ctypes.c_char_p),
("blinding", ctypes.c_void_p),
("mt_blinding", ctypes.c_void_p)
]
openssl.RSA_PKCS1_PADDING = 1
openssl.RSA_new.restype = ctypes.POINTER(RSA)
openssl.BN_bin2bn.restype = ctypes.c_void_p
openssl.BN_bin2bn.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_void_p]
openssl.BN_new.restype = ctypes.c_void_p
openssl.RSA_size.restype = ctypes.c_int
openssl.RSA_size.argtypes = [ctypes.POINTER(RSA)]
openssl.RSA_free.argtypes = [ctypes.POINTER(RSA)]
openssl.PEM_write_RSAPublicKey.restype = ctypes.c_int
openssl.PEM_write_RSAPublicKey.argtypes = [ctypes.c_void_p,
ctypes.POINTER(RSA)]
openssl.ERR_get_error.restype = ctypes.c_long
openssl.ERR_get_error.argtypes = []
openssl.ERR_error_string_n.restype = ctypes.c_void_p
openssl.ERR_error_string_n.argtypes = [ctypes.c_long,
ctypes.c_char_p,
ctypes.c_int]
openssl.ERR_load_crypto_strings.restype = ctypes.c_int
openssl.ERR_load_crypto_strings.argtypes = []
clib.fopen.restype = ctypes.c_void_p
clib.fopen.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
clib.fclose.restype = ctypes.c_int
clib.fclose.argtypes = [ctypes.c_void_p]
class CryptException(Exception):
pass
class OpenSSLException(CryptException):
def __init__(self):
message = self._get_openssl_error_msg()
super(OpenSSLException, self).__init__(message)
def _get_openssl_error_msg(self):
openssl.ERR_load_crypto_strings()
errno = openssl.ERR_get_error()
errbuf = ctypes.create_string_buffer(1024)
openssl.ERR_error_string_n(errno, errbuf, 1024)
return errbuf.value.decode("ascii")
class RSAWrapper(object):
def __init__(self, rsa_p):
self._rsa_p = rsa_p
def __enter__(self):
return self
def __exit__(self, tp, value, tb):
self.free()
def free(self):
openssl.RSA_free(self._rsa_p)
def public_encrypt(self, clear_text):
flen = len(clear_text)
rsa_size = openssl.RSA_size(self._rsa_p)
enc_text = ctypes.create_string_buffer(rsa_size)
enc_text_len = openssl.RSA_public_encrypt(flen,
clear_text,
enc_text,
self._rsa_p,
openssl.RSA_PKCS1_PADDING)
if enc_text_len == -1:
raise OpenSSLException()
return enc_text[:enc_text_len]
class CryptManager(object):
def load_ssh_rsa_public_key(self, ssh_pub_key):
ssh_rsa_prefix = "ssh-rsa "
i = ssh_pub_key.rindex(' ')
b64_pub_key = ssh_pub_key[len(ssh_rsa_prefix):i]
pub_key = base64.b64decode(b64_pub_key)
offset = 0
key_type_len = struct.unpack('>I', pub_key[offset:offset + 4])[0]
offset += 4
key_type = pub_key[offset:offset + key_type_len]
offset += key_type_len
if not key_type in ['ssh-rsa', 'rsa', 'rsa1']:
raise CryptException('Unsupported SSH key type "%s". '
'Only RSA keys are currently supported'
% key_type)
rsa_p = openssl.RSA_new()
try:
rsa_p.contents.e = openssl.BN_new()
rsa_p.contents.n = openssl.BN_new()
e_len = struct.unpack('>I', pub_key[offset:offset + 4])[0]
offset += 4
e_key_bin = pub_key[offset:offset + e_len]
offset += e_len
if not openssl.BN_bin2bn(e_key_bin, e_len, rsa_p.contents.e):
raise OpenSSLException()
n_len = struct.unpack('>I', pub_key[offset:offset + 4])[0]
offset += 4
n_key_bin = pub_key[offset:offset + n_len]
offset += n_len
if offset != len(pub_key):
raise CryptException('Invalid SSH key')
if not openssl.BN_bin2bn(n_key_bin, n_len, rsa_p.contents.n):
raise OpenSSLException()
return RSAWrapper(rsa_p)
except:
openssl.RSA_free(rsa_p)
raise