Add Keystone authentication to rest API

Authenticate requests via keystone to the REST API.

Change-Id: Ie45e5325c2f9cd892c60ff37acfcd1ceecc82e32
This commit is contained in:
Steven Dake 2014-11-18 13:14:34 -07:00
parent f00ab5ae7d
commit 62bfbf6045
3 changed files with 219 additions and 1 deletions

161
magnum/api/auth.py Normal file
View File

@ -0,0 +1,161 @@
# -*- 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 keystoneclient.middleware import auth_token
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
LOG = logging.getLogger(__name__)
OPT_GROUP_NAME = 'keystone_authtoken'
AUTH_OPTS = [
cfg.BoolOpt('enable_authentication',
default=True,
help='This option enables or disables user authentication '
'via keystone. Default value is True.'),
]
CONF = cfg.CONF
CONF.register_opts(AUTH_OPTS)
CONF.register_opts(auth_token.opts, group=OPT_GROUP_NAME)
PUBLIC_ENDPOINTS = [
'^/?$',
'^/v[0-9]+/?$',
'^/v[0-9]+/triggers',
'^/camp/platform_endpoints',
'^/camp/camp_v1_1_endpoint'
]
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
"""
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')
if user_id is None:
LOG.debug("X-User-Id header was not found in the request")
raise Exception('Not authorized')
roles = self._get_roles(state.request)
project_id = headers.get('X-Project-Id')
user_name = headers.get('X-User-Name', '')
domain = headers.get('X-Domain-Name')
project_domain_id = headers.get('X-Project-Domain-Id', '')
user_domain_id = headers.get('X-User-Domain-Id', '')
# 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('keystoneclient.middleware.auth_token')
auth_url = cfg.CONF.keystone_authtoken.auth_uri
auth_token_info = state.request.environ.get('keystone.token_info')
identity_status = headers.get('X-Identity-Status')
if identity_status == 'Confirmed':
ctx = context.RequestContext(auth_token=recv_auth_token,
auth_token_info=auth_token_info,
user=user_id,
tenant=project_id,
domain=domain,
user_domain=user_domain_id,
project_domain=project_domain_id,
user_name=user_name,
roles=roles,
auth_url=auth_url)
state.request.security_context = ctx
else:
LOG.debug("The provided identity is not confirmed.")
raise Exception('Not authorized. Identity not confirmed.')
return
def _get_roles(self, req):
"""Get the list of roles."""
if 'X-Roles' in req.headers:
roles = req.headers.get('X-Roles', '')
else:
# Fallback to deprecated role header:
roles = req.headers.get('X-Role', '')
if roles:
LOG.warn(_("X-Roles is missing. Using deprecated X-Role "
"header"))
return [r.strip() for r in roles.split(',')]
AUTH = AuthHelper()

View File

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

54
magnum/common/context.py Normal file
View File

@ -0,0 +1,54 @@
# Copyright 2014 - Mirantis, 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 inspect
from magnum.openstack.common import context
class RequestContext(context.RequestContext):
def __init__(self, auth_token=None, user=None, tenant=None, domain=None,
user_domain=None, project_domain=None, is_admin=False,
read_only=False, request_id=None, user_name=None, roles=None,
auth_url=None, trust_id=None, auth_token_info=None):
super(RequestContext, self).__init__(auth_token=auth_token,
user=user, tenant=tenant,
domain=domain,
user_domain=user_domain,
project_domain=project_domain,
is_admin=is_admin,
read_only=read_only,
show_deleted=False,
request_id=request_id)
self.roles = roles
self.user_name = user_name
self.auth_url = auth_url
self.trust_id = trust_id
self.auth_token_info = auth_token_info
def to_dict(self):
data = super(RequestContext, self).to_dict()
data.update(roles=self.roles, user_name=self.user_name,
auth_url=self.auth_url,
auth_token_info=self.auth_token_info,
trust_id=self.trust_id)
return data
@classmethod
def from_dict(cls, values):
allowed = [arg for arg in
inspect.getargspec(RequestContext.__init__).args
if arg != 'self']
kwargs = dict((k, v) for (k, v) in values.items() if k in allowed)
return cls(**kwargs)