From 43af5e45c71dbab812cd48f2f536f7f70ef23000 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 26 Jul 2016 14:07:30 -0400 Subject: [PATCH] Add noauth middleware This patch implements noauth middleware that can be enabled by adding the following to heat.conf: [paste_deploy] flavor = noauth One use case for this middleware would be to use alongside of a single process heat-all setup (using fake_rpc, sqlite) to avoid having to bootstrap keystone to use only the Heat software deployments resources. We could use this approach to help bootstrap TripleO's undercloud using heat templates with pre-deployed servers (a single undercloud server for the intial case). Change-Id: I50a8cc46b4c3c235d438a711760fba94bf8e9715 --- etc/heat/api-paste.ini | 11 ++++++ heat/common/noauth.py | 70 +++++++++++++++++++++++++++++++++++ heat/tests/test_noauth.py | 77 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 heat/common/noauth.py create mode 100644 heat/tests/test_noauth.py diff --git a/etc/heat/api-paste.ini b/etc/heat/api-paste.ini index b1068efd84..986a4a2528 100644 --- a/etc/heat/api-paste.ini +++ b/etc/heat/api-paste.ini @@ -22,6 +22,13 @@ pipeline = cors request_id faultwrap http_proxy_to_wsgi versionnegotiation authu [pipeline:heat-api-custombackend] pipeline = cors request_id faultwrap versionnegotiation context custombackendauth apiv1app +# To enable, in heat.conf: +# [paste_deploy] +# flavor = noauth +# +[pipeline:heat-api-noauth] +pipeline = cors request_id faultwrap http_proxy_to_wsgi versionnegotiation noauth context apiv1app + # heat-api-cfn pipeline [pipeline:heat-api-cfn] pipeline = cors http_proxy_to_wsgi cfnversionnegotiation osprofiler ec2authtoken authtoken context apicfnv1app @@ -97,6 +104,10 @@ paste.filter_factory = heat.common.auth_password:filter_factory [filter:custombackendauth] paste.filter_factory = heat.common.custom_backend_auth:filter_factory +# Auth middleware that accepts any auth +[filter:noauth] +paste.filter_factory = heat.common.noauth:filter_factory + # Middleware to set x-openstack-request-id in http response header [filter:request_id] paste.filter_factory = oslo_middleware.request_id:RequestId.factory diff --git a/heat/common/noauth.py b/heat/common/noauth.py new file mode 100644 index 0000000000..a111c279e4 --- /dev/null +++ b/heat/common/noauth.py @@ -0,0 +1,70 @@ +# +# Copyright (C) 2016, Red Hat, Inc. +# +# 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. + +"""Middleware that accepts any authentication.""" + +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +class NoAuthProtocol(object): + def __init__(self, app, conf): + self.conf = conf + self.app = app + + def __call__(self, env, start_response): + """Handle incoming request. + + Authenticate send downstream on success. Reject request if + we can't authenticate. + """ + LOG.debug('Authenticating user token') + env.update(self._build_user_headers(env)) + return self.app(env, start_response) + + def _build_user_headers(self, env): + """Build headers that represent authenticated user from auth token.""" + + # token = env.get('X-Auth-Token', '') + # user_id, _sep, project_id = token.partition(':') + # project_id = project_id or user_id + + username = env.get('HTTP_X_AUTH_USER', 'admin') + project = env.get('HTTP_X_AUTH_PROJECT', 'admin') + + headers = { + 'HTTP_X_IDENTITY_STATUS': 'Confirmed', + 'HTTP_X_PROJECT_ID': project, + 'HTTP_X_PROJECT_NAME': project, + 'HTTP_X_USER_ID': username, + 'HTTP_X_USER_NAME': username, + 'HTTP_X_ROLES': 'admin', + 'HTTP_X_SERVICE_CATALOG': {}, + 'HTTP_X_AUTH_USER': username, + 'HTTP_X_AUTH_KEY': 'unset', + } + + return headers + + +def filter_factory(global_conf, **local_conf): + conf = global_conf.copy() + conf.update(local_conf) + + def auth_filter(app): + return NoAuthProtocol(app, conf) + return auth_filter diff --git a/heat/tests/test_noauth.py b/heat/tests/test_noauth.py new file mode 100644 index 0000000000..7a524ccb4d --- /dev/null +++ b/heat/tests/test_noauth.py @@ -0,0 +1,77 @@ +# +# Copyright (C) 2016, Red Hat, Inc. +# +# 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 six +import webob + +from heat.common import noauth +from heat.tests import common + +EXPECTED_ENV_RESPONSE = { + 'HTTP_X_IDENTITY_STATUS': 'Confirmed', + 'HTTP_X_PROJECT_ID': 'admin', + 'HTTP_X_PROJECT_NAME': 'admin', + 'HTTP_X_USER_ID': 'admin', + 'HTTP_X_USER_NAME': 'admin', + 'HTTP_X_ROLES': 'admin', + 'HTTP_X_SERVICE_CATALOG': {}, + 'HTTP_X_AUTH_USER': 'admin', + 'HTTP_X_AUTH_KEY': 'unset', +} + + +class FakeApp(object): + """This represents a WSGI app protected by our auth middleware.""" + + def __init__(self, expected_env=None): + expected_env = expected_env or {} + self.expected_env = dict(EXPECTED_ENV_RESPONSE) + self.expected_env.update(expected_env) + + def __call__(self, env, start_response): + """Assert that expected environment is present when finally called.""" + for k, v in self.expected_env.items(): + assert env[k] == v, '%s != %s' % (env[k], v) + resp = webob.Response() + resp.body = six.b('SUCCESS') + return resp(env, start_response) + + +class KeystonePasswordAuthProtocolTest(common.HeatTestCase): + + def setUp(self): + super(KeystonePasswordAuthProtocolTest, self).setUp() + self.config = {'auth_uri': 'http://keystone.test.com:5000'} + self.app = FakeApp() + self.middleware = noauth.NoAuthProtocol( + self.app, self.config) + + def _start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + + def test_request_with_bad_credentials(self): + req = webob.Request.blank('/tenant_id1/') + req.headers['X_AUTH_USER'] = 'admin' + req.headers['X_AUTH_KEY'] = 'blah' + req.headers['X_AUTH_URL'] = self.config['auth_uri'] + self.middleware(req.environ, self._start_fake_response) + self.assertEqual(200, self.response_status) + + def test_request_with_no_tenant_in_url_or_auth_headers(self): + req = webob.Request.blank('/') + self.middleware(req.environ, self._start_fake_response) + self.assertEqual(200, self.response_status)