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', {}),
**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 -*-
#
# 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)

View File

@ -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
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.
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)

View File

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

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
# 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::