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:
parent
0dc3a1e3ab
commit
bd8b68e141
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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():
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user