]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Enable multi-cloud standalone mode
authorBen Nemec <openstack@nemebean.com>
Sun, 11 Aug 2013 23:15:17 +0000 (18:15 -0500)
committerBen Nemec <openstack@nemebean.com>
Mon, 12 Aug 2013 03:16:26 +0000 (22:16 -0500)
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

etc/heat/heat.conf.sample
heat/common/auth_password.py
heat/common/config.py
heat/tests/test_auth_password.py

index 68e42f817589e67c29c964260dac8461a235d06b..fba4e730e6a09283b27bef58699298244c337e93 100644 (file)
 #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]
 
 #
 #ringfile=/etc/oslo/matchmaker_ring.json
 
 
-# Total option count: 107
+# Total option count: 109
index 0d354ca1476b8bfe2b2f0c7d1b5e496e2e50134e..9164710d38eee7db34ede26f64c0bb6e746a40a2 100644 (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."""
index f88a90d591d9fb741e9c997f9a3dae368b7ce191..87533868cff33cad210d35e8576f591c38e28f97 100644 (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():
index 9974fceadf07e255a2bb640081a73e198c0a711e..4aa156409a4dd27d3a59abb4ba97d30af5f33e09 100644 (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)