Added support for post metadata encrypted password
This commit is contained in:
parent
11e183212f
commit
e94d732d2b
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
0
cloudbaseinit/utils/__init__.py
Normal file
0
cloudbaseinit/utils/__init__.py
Normal file
190
cloudbaseinit/utils/crypt.py
Normal file
190
cloudbaseinit/utils/crypt.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user