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:
OTSUKA, Yuanying 2014-12-26 13:50:40 +09:00 committed by Motohiro OTSUKA
parent a6f4f0f137
commit c6c606b277
10 changed files with 245 additions and 159 deletions

View File

@ -54,4 +54,5 @@ def setup_app(config=None):
logging=getattr(config, 'logging', {}), logging=getattr(config, 'logging', {}),
**app_conf **app_conf
) )
return auth.install(app, CONF)
return auth.install(app, CONF, config.app.acl_public_routes)

View File

@ -1,5 +1,8 @@
# -*- encoding: utf-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 # 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 # 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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
import re """Access Control Lists (ACL's) control access the API server."""
from keystonemiddleware import auth_token
from oslo.config import cfg from oslo.config import cfg
from oslo.utils import importutils
from pecan import hooks
from magnum.common import context from magnum.api.middleware import auth_token
from magnum.openstack.common._i18n import _
from magnum.openstack.common import log as logging
LOG = logging.getLogger(__name__)
OPT_GROUP_NAME = 'keystone_authtoken'
AUTH_OPTS = [ AUTH_OPTS = [
cfg.BoolOpt('enable_authentication', cfg.BoolOpt('enable_authentication',
default=True, default=True,
@ -39,99 +32,19 @@ AUTH_OPTS = [
CONF = cfg.CONF CONF = cfg.CONF
CONF.register_opts(AUTH_OPTS) CONF.register_opts(AUTH_OPTS)
PUBLIC_ENDPOINTS = [
"^/$"
]
def install(app, conf, public_routes):
"""Install ACL check on application.
def install(app, conf): :param app: A WSGI applicatin.
if conf.get('enable_authentication'): :param conf: Settings. Dict'ified and passed to keystonemiddleware
return AuthProtocolWrapper(app, conf=dict(conf.get(OPT_GROUP_NAME))) :param public_routes: The list of the routes which will be allowed to
else: access without authentication.
LOG.warning(_('Keystone authentication is disabled by Magnum ' :return: The same WSGI application with ACL installed.
'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
""" """
if not cfg.CONF.get('enable_authentication'):
def __call__(self, env, start_response): return app
path = env.get('PATH_INFO') return auth_token.AuthTokenMiddleware(app,
if AUTH.is_endpoint_public(path): conf=dict(conf),
return self._app(env, start_response) public_api_routes=public_routes)
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()

View File

@ -14,14 +14,19 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from magnum.api import auth from magnum.api import hooks
# Pecan Application Configurations # Pecan Application Configurations
app = { app = {
'root': 'magnum.api.controllers.root.RootController', 'root': 'magnum.api.controllers.root.RootController',
'modules': ['magnum.api'], 'modules': ['magnum.api'],
'debug': False, 'debug': False,
'hooks': [auth.AuthInformationHook()] 'hooks': [
hooks.ContextHook()
],
'acl_public_routes': [
'/'
],
} }
# Custom Configurations must be in Python dictionary format:: # Custom Configurations must be in Python dictionary format::

67
magnum/api/hooks.py Normal file
View 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)

View 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)

View 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)

View File

@ -14,8 +14,8 @@
# limitations under the License. # limitations under the License.
from magnum.api import app as api_app 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 config as api_config
from magnum.api import hooks
from magnum.tests import base 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['modules'], api_config.app['modules'])
self.assertEqual(config_d['root'], api_config.app['root']) 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)

View File

@ -17,12 +17,11 @@ import mock
from oslo.config import fixture from oslo.config import fixture
from magnum.api import auth from magnum.api import auth
from magnum.common import context
from magnum.tests import base from magnum.tests import base
from magnum.tests import fakes from magnum.tests import fakes
@mock.patch('magnum.api.auth.AuthProtocolWrapper', @mock.patch('magnum.api.middleware.auth_token.AuthTokenMiddleware',
new_callable=fakes.FakeAuthProtocol) new_callable=fakes.FakeAuthProtocol)
class TestAuth(base.BaseTestCase): class TestAuth(base.BaseTestCase):
@ -32,51 +31,11 @@ class TestAuth(base.BaseTestCase):
self.app = fakes.FakeApp() self.app = fakes.FakeApp()
def test_check_auth_option_enabled(self, mock_auth): 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) 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) self.assertIsInstance(result, fakes.FakeAuthProtocol)
def test_check_auth_option_disabled(self, mock_auth): 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) 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) 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)

View 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)

View File

@ -10,6 +10,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from magnum.api import hooks
# Server Specific Configurations # Server Specific Configurations
server = { server = {
'port': '8080', 'port': '8080',
@ -20,13 +22,13 @@ server = {
app = { app = {
'root': 'magnum.api.controllers.root.RootController', 'root': 'magnum.api.controllers.root.RootController',
'modules': ['magnum.api'], 'modules': ['magnum.api'],
'static_root': '%(confdir)s/../../public',
'template_path': '%(confdir)s/../templates',
'debug': True, 'debug': True,
'errors': { 'hooks': [
'404': '/error/404', hooks.ContextHook(),
'__force_dict__': True ],
} 'acl_public_routes': [
'/'
],
} }
# Custom Configurations must be in Python dictionary format:: # Custom Configurations must be in Python dictionary format::