Enable multi-cloud standalone mode

Adds a multi_cloud configuration parameter for auth_password that
tells a standalone Heat installation to look for its auth_url in
the request headers instead of the configuration file.  Also adds
an allowed_auth_uris configuration option to specify which
auth_urls are allowed as targets.

bp heat-multicloud

Change-Id: Ic0929586ac3b57c7f9f3335e0dbd5e14e6b56067
This commit is contained in:
Ben Nemec 2013-08-11 18:15:17 -05:00
parent 0dc3a1e3ab
commit bd8b68e141
4 changed files with 120 additions and 15 deletions

View File

@ -482,6 +482,20 @@
#password=<None>
[auth_password]
#
# Options defined in heat.common.config
#
# Allow orchestration of multiple clouds (boolean value)
#multi_cloud=false
# Allowed targets for auth_uri when multi_cloud is enabled.
# If empty, all targets will be allowed. (list value)
#allowed_auth_uris=
[matchmaker_ring]
#
@ -492,4 +506,4 @@
#ringfile=/etc/oslo/matchmaker_ring.json
# Total option count: 107
# Total option count: 109

View File

@ -18,6 +18,7 @@
from keystoneclient.v2_0 import client as keystone_client
from keystoneclient import exceptions as keystone_exceptions
from oslo.config import cfg
from webob.exc import HTTPBadRequest
from webob.exc import HTTPUnauthorized
from heat.openstack.common import importutils
@ -34,12 +35,15 @@ class KeystonePasswordAuthProtocol(object):
def __init__(self, app, conf):
self.app = app
self.conf = conf
if 'auth_uri' in self.conf:
auth_url = self.conf['auth_uri']
else:
# Import auth_token to have keystone_authtoken settings setup.
importutils.import_module('keystoneclient.middleware.auth_token')
auth_url = cfg.CONF.keystone_authtoken['auth_uri']
auth_url = None
if not cfg.CONF.auth_password.multi_cloud:
if 'auth_uri' in self.conf:
auth_url = self.conf['auth_uri']
else:
# Import auth_token to have keystone_authtoken settings setup.
importutils.import_module(
'keystoneclient.middleware.auth_token')
auth_url = cfg.CONF.keystone_authtoken['auth_uri']
self.auth_url = auth_url
def __call__(self, env, start_response):
@ -48,28 +52,34 @@ class KeystonePasswordAuthProtocol(object):
password = env.get('HTTP_X_AUTH_KEY')
# Determine tenant id from path.
tenant = env.get('PATH_INFO').split('/')[1]
auth_url = self.auth_url
if cfg.CONF.auth_password.multi_cloud:
auth_url = env.get('HTTP_X_AUTH_URL')
error = self._validate_auth_url(env, start_response, auth_url)
if error:
return error
if not tenant:
return self._reject_request(env, start_response)
return self._reject_request(env, start_response, auth_url)
try:
client = keystone_client.Client(
username=username, password=password, tenant_id=tenant,
auth_url=self.auth_url)
auth_url=auth_url)
except (keystone_exceptions.Unauthorized,
keystone_exceptions.Forbidden,
keystone_exceptions.NotFound,
keystone_exceptions.AuthorizationFailure):
return self._reject_request(env, start_response)
return self._reject_request(env, start_response, auth_url)
env['keystone.token_info'] = client.auth_ref
env.update(self._build_user_headers(client.auth_ref))
env.update(self._build_user_headers(client.auth_ref, auth_url))
return self.app(env, start_response)
def _reject_request(self, env, start_response):
def _reject_request(self, env, start_response, auth_url):
"""Redirect client to auth server."""
headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % self.auth_url)]
headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % auth_url)]
resp = HTTPUnauthorized('Authentication required', headers)
return resp(env, start_response)
def _build_user_headers(self, token_info):
def _build_user_headers(self, token_info, auth_url):
"""Build headers that represent authenticated user from auth token."""
tenant_id = token_info['token']['tenant']['id']
tenant_name = token_info['token']['tenant']['name']
@ -89,7 +99,7 @@ class KeystonePasswordAuthProtocol(object):
'HTTP_X_ROLES': roles,
'HTTP_X_SERVICE_CATALOG': service_catalog,
'HTTP_X_AUTH_TOKEN': auth_token,
'HTTP_X_AUTH_URL': self.auth_url,
'HTTP_X_AUTH_URL': auth_url,
# DEPRECATED
'HTTP_X_USER': user_name,
'HTTP_X_TENANT_ID': tenant_id,
@ -100,6 +110,19 @@ class KeystonePasswordAuthProtocol(object):
return headers
def _validate_auth_url(self, env, start_response, auth_url):
"""Validate auth_url to ensure it can be used."""
if not auth_url:
resp = HTTPBadRequest(_('Request missing required header '
'X-Auth-Url'))
return resp(env, start_response)
allowed = cfg.CONF.auth_password.allowed_auth_uris
if allowed and not auth_url in allowed:
resp = HTTPUnauthorized(_('Header X-Auth-Url "%s" not allowed')
% auth_url)
return resp(env, start_response)
return None
def filter_factory(global_conf, **local_conf):
"""Returns a WSGI filter app for use with paste.deploy."""

View File

@ -111,12 +111,24 @@ rpc_opts = [
'This can be an opaque identifier.'
'It is not necessarily a hostname, FQDN, or IP address.')]
auth_password_group = cfg.OptGroup('auth_password')
auth_password_opts = [
cfg.BoolOpt('multi_cloud',
default=False,
help=_('Allow orchestration of multiple clouds')),
cfg.ListOpt('allowed_auth_uris',
default=[],
help=_('Allowed targets for auth_uri when multi_cloud is '
'enabled. If empty, all targets will be allowed.'))]
cfg.CONF.register_opts(db_opts)
cfg.CONF.register_opts(engine_opts)
cfg.CONF.register_opts(service_opts)
cfg.CONF.register_opts(rpc_opts)
cfg.CONF.register_group(paste_deploy_group)
cfg.CONF.register_opts(paste_deploy_opts, group=paste_deploy_group)
cfg.CONF.register_group(auth_password_group)
cfg.CONF.register_opts(auth_password_opts, group=auth_password_group)
def rpc_set_default():

View File

@ -17,6 +17,7 @@
from keystoneclient.v2_0 import client as keystone_client
from keystoneclient.exceptions import Unauthorized
from oslo.config import cfg
import webob
from heat.common.auth_password import KeystonePasswordAuthProtocol
@ -81,6 +82,11 @@ class KeystonePasswordAuthProtocolTest(HeatTestCase):
expected_env={'HTTP_X_AUTH_URL': self.config['auth_uri']})
self.middleware = KeystonePasswordAuthProtocol(self.app, self.config)
def tearDown(self):
super(KeystonePasswordAuthProtocolTest, self).tearDown()
cfg.CONF.clear_override('multi_cloud', 'auth_password')
cfg.CONF.clear_override('allowed_auth_uris', 'auth_password')
def _start_fake_response(self, status, headers):
self.response_status = int(status.split(' ', 1)[0])
self.response_headers = dict(headers)
@ -117,3 +123,53 @@ class KeystonePasswordAuthProtocolTest(HeatTestCase):
req = webob.Request.blank('/')
self.middleware(req.environ, self._start_fake_response)
self.assertEqual(self.response_status, 401)
def _test_multi_cloud(self, allowed_auth_uris=[]):
cfg.CONF.set_override('multi_cloud', True, group='auth_password')
auth_url = 'http://multicloud.test.com:5000/v2.0'
cfg.CONF.set_override('allowed_auth_uris',
allowed_auth_uris,
group='auth_password')
self.app = FakeApp(
expected_env={'HTTP_X_AUTH_URL': auth_url})
self.middleware = KeystonePasswordAuthProtocol(self.app, self.config)
self.m.StubOutClassWithMocks(keystone_client, 'Client')
mock_client = keystone_client.Client(
username='user_name1', password='goodpassword',
tenant_id='tenant_id1', auth_url=auth_url)
mock_client.auth_ref = TOKEN_RESPONSE
self.m.ReplayAll()
req = webob.Request.blank('/tenant_id1/')
req.headers['X_AUTH_USER'] = 'user_name1'
req.headers['X_AUTH_KEY'] = 'goodpassword'
req.headers['X_AUTH_URL'] = auth_url
self.middleware(req.environ, self._start_fake_response)
self.m.VerifyAll()
def test_multi_cloud(self):
self._test_multi_cloud(['http://multicloud.test.com:5000/v2.0'])
def test_multi_cloud_empty_allowed_uris(self):
self._test_multi_cloud()
def test_multi_cloud_target_not_allowed(self):
cfg.CONF.set_override('multi_cloud', True, group='auth_password')
auth_url = 'http://multicloud.test.com:5000/v2.0'
cfg.CONF.set_override('allowed_auth_uris',
['http://some.other.url:5000/v2.0'],
group='auth_password')
req = webob.Request.blank('/tenant_id1/')
req.headers['X_AUTH_USER'] = 'user_name1'
req.headers['X_AUTH_KEY'] = 'goodpassword'
req.headers['X_AUTH_URL'] = auth_url
self.middleware(req.environ, self._start_fake_response)
self.assertEqual(self.response_status, 401)
def test_multi_cloud_no_auth_url(self):
cfg.CONF.set_override('multi_cloud', True, group='auth_password')
req = webob.Request.blank('/tenant_id1/')
req.headers['X_AUTH_USER'] = 'user_name1'
req.headers['X_AUTH_KEY'] = 'goodpassword'
response = self.middleware(req.environ, self._start_fake_response)
self.assertEqual(self.response_status, 400)