Split up extracting auth.py file
The auth.py file does a couple of things, It contains keystone authentication and to set request context. So this split up to two files. After this commit, request hook should be included in hooks.py. Related-Bug: #1406539 Change-Id: I1754da40383976e48f6fd4ca23911717f918f9bb
This commit is contained in:
parent
a6f4f0f137
commit
c6c606b277
@ -54,4 +54,5 @@ def setup_app(config=None):
|
||||
logging=getattr(config, 'logging', {}),
|
||||
**app_conf
|
||||
)
|
||||
return auth.install(app, CONF)
|
||||
|
||||
return auth.install(app, CONF, config.app.acl_public_routes)
|
||||
|
@ -1,5 +1,8 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
|
||||
#
|
||||
# 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
|
||||
@ -13,22 +16,12 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
|
||||
from keystonemiddleware import auth_token
|
||||
"""Access Control Lists (ACL's) control access the API server."""
|
||||
from oslo.config import cfg
|
||||
from oslo.utils import importutils
|
||||
from pecan import hooks
|
||||
|
||||
from magnum.common import context
|
||||
from magnum.openstack.common._i18n import _
|
||||
from magnum.openstack.common import log as logging
|
||||
from magnum.api.middleware import auth_token
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
OPT_GROUP_NAME = 'keystone_authtoken'
|
||||
|
||||
AUTH_OPTS = [
|
||||
cfg.BoolOpt('enable_authentication',
|
||||
default=True,
|
||||
@ -39,99 +32,19 @@ AUTH_OPTS = [
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(AUTH_OPTS)
|
||||
|
||||
PUBLIC_ENDPOINTS = [
|
||||
"^/$"
|
||||
]
|
||||
|
||||
def install(app, conf, public_routes):
|
||||
"""Install ACL check on application.
|
||||
|
||||
def install(app, conf):
|
||||
if conf.get('enable_authentication'):
|
||||
return AuthProtocolWrapper(app, conf=dict(conf.get(OPT_GROUP_NAME)))
|
||||
else:
|
||||
LOG.warning(_('Keystone authentication is disabled by Magnum '
|
||||
'configuration parameter enable_authentication. '
|
||||
'Magnum will not authenticate incoming request. '
|
||||
'In order to enable authentication set '
|
||||
'enable_authentication option to True.'))
|
||||
|
||||
return app
|
||||
|
||||
|
||||
class AuthHelper(object):
|
||||
"""Helper methods for Auth."""
|
||||
|
||||
def __init__(self):
|
||||
endpoints_pattern = '|'.join(pe for pe in PUBLIC_ENDPOINTS)
|
||||
self._public_endpoints_regexp = re.compile(endpoints_pattern)
|
||||
|
||||
def is_endpoint_public(self, path):
|
||||
return self._public_endpoints_regexp.match(path)
|
||||
|
||||
|
||||
class AuthProtocolWrapper(auth_token.AuthProtocol):
|
||||
"""A wrapper on Keystone auth_token AuthProtocol.
|
||||
|
||||
Does not perform verification of authentication tokens for pub routes in
|
||||
the API. Public routes are those defined by PUBLIC_ENDPOINTS
|
||||
:param app: A WSGI applicatin.
|
||||
:param conf: Settings. Dict'ified and passed to keystonemiddleware
|
||||
:param public_routes: The list of the routes which will be allowed to
|
||||
access without authentication.
|
||||
:return: The same WSGI application with ACL installed.
|
||||
|
||||
"""
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
path = env.get('PATH_INFO')
|
||||
if AUTH.is_endpoint_public(path):
|
||||
return self._app(env, start_response)
|
||||
return super(AuthProtocolWrapper, self).__call__(env, start_response)
|
||||
|
||||
|
||||
class AuthInformationHook(hooks.PecanHook):
|
||||
|
||||
def before(self, state):
|
||||
if not CONF.get('enable_authentication'):
|
||||
return
|
||||
# Skip authentication for public endpoints
|
||||
if AUTH.is_endpoint_public(state.request.path):
|
||||
return
|
||||
|
||||
headers = state.request.headers
|
||||
user_id = headers.get('X-User-Id')
|
||||
user_id = headers.get('X-User', user_id)
|
||||
if user_id is None:
|
||||
LOG.debug("X-User-Id header was not found in the request")
|
||||
raise Exception('Not authorized')
|
||||
|
||||
tenant = state.request.headers.get('X-Tenant-Id')
|
||||
tenant = state.request.headers.get('X-Tenant', tenant)
|
||||
domain_id = state.request.headers.get('X-User-Domain-Id')
|
||||
domain_name = state.request.headers.get('X-User-Domain-Name')
|
||||
auth_token_info = state.request.environ.get('keystone.token_info')
|
||||
|
||||
# Get the auth token
|
||||
try:
|
||||
recv_auth_token = headers.get('X-Auth-Token',
|
||||
headers.get(
|
||||
'X-Storage-Token'))
|
||||
except ValueError:
|
||||
LOG.debug("No auth token found in the request.")
|
||||
raise Exception('Not authorized')
|
||||
auth_url = headers.get('X-Auth-Url')
|
||||
if auth_url is None:
|
||||
importutils.import_module('keystonemiddleware.auth_token')
|
||||
auth_url = cfg.CONF.keystone_authtoken.auth_uri
|
||||
|
||||
identity_status = headers.get('X-Identity-Status')
|
||||
if identity_status == 'Confirmed':
|
||||
ctx = context.RequestContext(auth_token=recv_auth_token,
|
||||
auth_url=auth_url,
|
||||
auth_token_info=auth_token_info,
|
||||
user=user_id,
|
||||
tenant=tenant,
|
||||
domain_id=domain_id,
|
||||
domain_name=domain_name)
|
||||
state.request.context = ctx
|
||||
else:
|
||||
LOG.debug("The provided identity is not confirmed.")
|
||||
raise Exception('Not authorized. Identity not confirmed.')
|
||||
return
|
||||
|
||||
|
||||
AUTH = AuthHelper()
|
||||
if not cfg.CONF.get('enable_authentication'):
|
||||
return app
|
||||
return auth_token.AuthTokenMiddleware(app,
|
||||
conf=dict(conf),
|
||||
public_api_routes=public_routes)
|
||||
|
@ -14,14 +14,19 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from magnum.api import auth
|
||||
from magnum.api import hooks
|
||||
|
||||
# Pecan Application Configurations
|
||||
app = {
|
||||
'root': 'magnum.api.controllers.root.RootController',
|
||||
'modules': ['magnum.api'],
|
||||
'debug': False,
|
||||
'hooks': [auth.AuthInformationHook()]
|
||||
'hooks': [
|
||||
hooks.ContextHook()
|
||||
],
|
||||
'acl_public_routes': [
|
||||
'/'
|
||||
],
|
||||
}
|
||||
|
||||
# Custom Configurations must be in Python dictionary format::
|
||||
|
67
magnum/api/hooks.py
Normal file
67
magnum/api/hooks.py
Normal file
@ -0,0 +1,67 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
from oslo.config import cfg
|
||||
from oslo.utils import importutils
|
||||
from pecan import hooks
|
||||
|
||||
from magnum.common import context
|
||||
|
||||
|
||||
class ContextHook(hooks.PecanHook):
|
||||
"""Configures a request context and attaches it to the request.
|
||||
|
||||
The following HTTP request headers are used:
|
||||
|
||||
X-User-Id or X-User:
|
||||
Used for context.user_id.
|
||||
|
||||
X-Tenant-Id or X-Tenant:
|
||||
Used for context.tenant.
|
||||
|
||||
X-Auth-Token:
|
||||
Used for context.auth_token.
|
||||
|
||||
"""
|
||||
|
||||
def before(self, state):
|
||||
headers = state.request.headers
|
||||
user_id = headers.get('X-User-Id')
|
||||
user_id = headers.get('X-User', user_id)
|
||||
tenant = state.request.headers.get('X-Tenant-Id')
|
||||
tenant = state.request.headers.get('X-Tenant', tenant)
|
||||
domain_id = state.request.headers.get('X-User-Domain-Id')
|
||||
domain_name = state.request.headers.get('X-User-Domain-Name')
|
||||
auth_token = state.request.headers.get('X-Storage-Token')
|
||||
auth_token = state.request.headers.get('X-Auth-Token', auth_token)
|
||||
auth_token_info = state.request.environ.get('keystone.token_info')
|
||||
|
||||
auth_url = headers.get('X-Auth-Url')
|
||||
if auth_url is None:
|
||||
importutils.import_module('keystonemiddleware.auth_token')
|
||||
auth_url = cfg.CONF.keystone_authtoken.auth_uri
|
||||
|
||||
state.request.context = context.RequestContext(
|
||||
auth_token=auth_token,
|
||||
auth_url=auth_url,
|
||||
auth_token_info=auth_token_info,
|
||||
user=user_id,
|
||||
tenant=tenant,
|
||||
domain_id=domain_id,
|
||||
domain_name=domain_name)
|
20
magnum/api/middleware/__init__.py
Normal file
20
magnum/api/middleware/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# 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.
|
||||
|
||||
from magnum.api.middleware import auth_token
|
||||
|
||||
|
||||
AuthTokenMiddleware = auth_token.AuthTokenMiddleware
|
||||
|
||||
__all__ = (AuthTokenMiddleware)
|
60
magnum/api/middleware/auth_token.py
Normal file
60
magnum/api/middleware/auth_token.py
Normal file
@ -0,0 +1,60 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# 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 re
|
||||
|
||||
from keystonemiddleware import auth_token
|
||||
|
||||
from magnum.common import exception
|
||||
from magnum.common import utils
|
||||
from magnum.openstack.common._i18n import _
|
||||
from magnum.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthTokenMiddleware(auth_token.AuthProtocol):
|
||||
"""A wrapper on Keystone auth_token middleware.
|
||||
|
||||
Does not perform verification of authentication tokens
|
||||
for public routes in the API.
|
||||
|
||||
"""
|
||||
def __init__(self, app, conf, public_api_routes=[]):
|
||||
route_pattern_tpl = '%s(\.json|\.xml)?$'
|
||||
|
||||
try:
|
||||
self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl)
|
||||
for route_tpl in public_api_routes]
|
||||
except re.error as e:
|
||||
msg = _('Cannot compile public API routes: %s') % e
|
||||
|
||||
LOG.error(msg)
|
||||
raise exception.ConfigInvalid(error_msg=msg)
|
||||
|
||||
super(AuthTokenMiddleware, self).__init__(app, conf)
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
path = utils.safe_rstrip(env.get('PATH_INFO'), '/')
|
||||
|
||||
# The information whether the API call is being performed against the
|
||||
# public API is required for some other components. Saving it to the
|
||||
# WSGI environment is reasonable thereby.
|
||||
env['is_public_api'] = any(map(lambda pattern: re.match(pattern, path),
|
||||
self.public_api_routes))
|
||||
|
||||
if env['is_public_api']:
|
||||
return self._app(env, start_response)
|
||||
|
||||
return super(AuthTokenMiddleware, self).__call__(env, start_response)
|
@ -14,8 +14,8 @@
|
||||
# limitations under the License.
|
||||
|
||||
from magnum.api import app as api_app
|
||||
from magnum.api import auth
|
||||
from magnum.api import config as api_config
|
||||
from magnum.api import hooks
|
||||
from magnum.tests import base
|
||||
|
||||
|
||||
@ -30,4 +30,4 @@ class TestAppConfig(base.BaseTestCase):
|
||||
|
||||
self.assertEqual(config_d['modules'], api_config.app['modules'])
|
||||
self.assertEqual(config_d['root'], api_config.app['root'])
|
||||
self.assertIsInstance(config_d['hooks'][0], auth.AuthInformationHook)
|
||||
self.assertIsInstance(config_d['hooks'][0], hooks.ContextHook)
|
||||
|
@ -17,12 +17,11 @@ import mock
|
||||
from oslo.config import fixture
|
||||
|
||||
from magnum.api import auth
|
||||
from magnum.common import context
|
||||
from magnum.tests import base
|
||||
from magnum.tests import fakes
|
||||
|
||||
|
||||
@mock.patch('magnum.api.auth.AuthProtocolWrapper',
|
||||
@mock.patch('magnum.api.middleware.auth_token.AuthTokenMiddleware',
|
||||
new_callable=fakes.FakeAuthProtocol)
|
||||
class TestAuth(base.BaseTestCase):
|
||||
|
||||
@ -32,51 +31,11 @@ class TestAuth(base.BaseTestCase):
|
||||
self.app = fakes.FakeApp()
|
||||
|
||||
def test_check_auth_option_enabled(self, mock_auth):
|
||||
|
||||
self.CONF.config(auth_protocol="footp",
|
||||
auth_version="v2.0",
|
||||
auth_uri=None,
|
||||
group=auth.OPT_GROUP_NAME)
|
||||
self.CONF.config(enable_authentication=True)
|
||||
result = auth.install(self.app, self.CONF.conf)
|
||||
result = auth.install(self.app, self.CONF.conf, ['/'])
|
||||
self.assertIsInstance(result, fakes.FakeAuthProtocol)
|
||||
|
||||
def test_check_auth_option_disabled(self, mock_auth):
|
||||
self.CONF.config(auth_protocol="footp",
|
||||
auth_version="v2.0",
|
||||
auth_uri=None,
|
||||
group=auth.OPT_GROUP_NAME)
|
||||
self.CONF.config(enable_authentication=False)
|
||||
result = auth.install(self.app, self.CONF.conf)
|
||||
result = auth.install(self.app, self.CONF.conf, ['/'])
|
||||
self.assertIsInstance(result, fakes.FakeApp)
|
||||
|
||||
def test_auth_hook_before_method(self, mock_cls):
|
||||
state = mock.Mock(request=fakes.FakePecanRequest())
|
||||
hook = auth.AuthInformationHook()
|
||||
hook.before(state)
|
||||
ctx = state.request.context
|
||||
self.assertIsInstance(ctx, context.RequestContext)
|
||||
self.assertEqual(ctx.auth_token,
|
||||
fakes.fakeAuthTokenHeaders['X-Auth-Token'])
|
||||
self.assertEqual(ctx.tenant,
|
||||
fakes.fakeAuthTokenHeaders['X-Tenant-Id'])
|
||||
self.assertEqual(ctx.user,
|
||||
fakes.fakeAuthTokenHeaders['X-User-Id'])
|
||||
self.assertEqual(ctx.auth_url,
|
||||
fakes.fakeAuthTokenHeaders['X-Auth-Url'])
|
||||
self.assertEqual(ctx.domain_name,
|
||||
fakes.fakeAuthTokenHeaders['X-User-Domain-Name'])
|
||||
self.assertEqual(ctx.domain_id,
|
||||
fakes.fakeAuthTokenHeaders['X-User-Domain-Id'])
|
||||
self.assertIsNone(ctx.auth_token_info)
|
||||
|
||||
def test_auth_hook_before_method_auth_info(self, mock_cls):
|
||||
state = mock.Mock(request=fakes.FakePecanRequest())
|
||||
state.request.environ['keystone.token_info'] = 'assert_this'
|
||||
hook = auth.AuthInformationHook()
|
||||
hook.before(state)
|
||||
ctx = state.request.context
|
||||
self.assertIsInstance(ctx, context.RequestContext)
|
||||
self.assertEqual(fakes.fakeAuthTokenHeaders['X-Auth-Token'],
|
||||
ctx.auth_token)
|
||||
self.assertEqual('assert_this', ctx.auth_token_info)
|
||||
|
59
magnum/tests/api/test_hooks.py
Normal file
59
magnum/tests/api/test_hooks.py
Normal file
@ -0,0 +1,59 @@
|
||||
# Copyright 2014
|
||||
# The Cloudscaling Group, 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 mock
|
||||
|
||||
from magnum.api import hooks
|
||||
from magnum.common import context
|
||||
from magnum.tests import base
|
||||
from magnum.tests import fakes
|
||||
|
||||
|
||||
class TestHooks(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestHooks, self).setUp()
|
||||
self.app = fakes.FakeApp()
|
||||
|
||||
def test_context_hook_before_method(self):
|
||||
state = mock.Mock(request=fakes.FakePecanRequest())
|
||||
hook = hooks.ContextHook()
|
||||
hook.before(state)
|
||||
ctx = state.request.context
|
||||
self.assertIsInstance(ctx, context.RequestContext)
|
||||
self.assertEqual(ctx.auth_token,
|
||||
fakes.fakeAuthTokenHeaders['X-Auth-Token'])
|
||||
self.assertEqual(ctx.tenant,
|
||||
fakes.fakeAuthTokenHeaders['X-Tenant-Id'])
|
||||
self.assertEqual(ctx.user,
|
||||
fakes.fakeAuthTokenHeaders['X-User-Id'])
|
||||
self.assertEqual(ctx.auth_url,
|
||||
fakes.fakeAuthTokenHeaders['X-Auth-Url'])
|
||||
self.assertEqual(ctx.domain_name,
|
||||
fakes.fakeAuthTokenHeaders['X-User-Domain-Name'])
|
||||
self.assertEqual(ctx.domain_id,
|
||||
fakes.fakeAuthTokenHeaders['X-User-Domain-Id'])
|
||||
self.assertIsNone(ctx.auth_token_info)
|
||||
|
||||
def test_context_hook_before_method_auth_info(self):
|
||||
state = mock.Mock(request=fakes.FakePecanRequest())
|
||||
state.request.environ['keystone.token_info'] = 'assert_this'
|
||||
hook = hooks.ContextHook()
|
||||
hook.before(state)
|
||||
ctx = state.request.context
|
||||
self.assertIsInstance(ctx, context.RequestContext)
|
||||
self.assertEqual(fakes.fakeAuthTokenHeaders['X-Auth-Token'],
|
||||
ctx.auth_token)
|
||||
self.assertEqual('assert_this', ctx.auth_token_info)
|
@ -10,6 +10,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from magnum.api import hooks
|
||||
|
||||
# Server Specific Configurations
|
||||
server = {
|
||||
'port': '8080',
|
||||
@ -20,13 +22,13 @@ server = {
|
||||
app = {
|
||||
'root': 'magnum.api.controllers.root.RootController',
|
||||
'modules': ['magnum.api'],
|
||||
'static_root': '%(confdir)s/../../public',
|
||||
'template_path': '%(confdir)s/../templates',
|
||||
'debug': True,
|
||||
'errors': {
|
||||
'404': '/error/404',
|
||||
'__force_dict__': True
|
||||
}
|
||||
'hooks': [
|
||||
hooks.ContextHook(),
|
||||
],
|
||||
'acl_public_routes': [
|
||||
'/'
|
||||
],
|
||||
}
|
||||
|
||||
# Custom Configurations must be in Python dictionary format::
|
||||
|
Loading…
Reference in New Issue
Block a user