207 lines
7.8 KiB
Python
207 lines
7.8 KiB
Python
# 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 keystoneauth1 import discover
|
|
from keystoneauth1 import exceptions as ksa_exceptions
|
|
from keystoneauth1 import plugin
|
|
from keystoneclient.v2_0 import client as v2_client
|
|
from keystoneclient.v3 import client as v3_client
|
|
from six.moves import urllib
|
|
|
|
from keystonemiddleware.auth_token import _auth
|
|
from keystonemiddleware.auth_token import _exceptions as ksm_exceptions
|
|
from keystonemiddleware.i18n import _
|
|
|
|
ACCESS_RULES_SUPPORT = '1'
|
|
|
|
|
|
class _RequestStrategy(object):
|
|
|
|
AUTH_VERSION = None
|
|
|
|
def __init__(self, adap, include_service_catalog=None,
|
|
requested_auth_interface=None):
|
|
self._include_service_catalog = include_service_catalog
|
|
self._requested_auth_interface = requested_auth_interface
|
|
|
|
def verify_token(self, user_token, allow_expired=False):
|
|
pass
|
|
|
|
|
|
class _V2RequestStrategy(_RequestStrategy):
|
|
|
|
AUTH_VERSION = (2, 0)
|
|
|
|
def __init__(self, adap, **kwargs):
|
|
super(_V2RequestStrategy, self).__init__(adap, **kwargs)
|
|
self._client = v2_client.Client(session=adap)
|
|
|
|
def verify_token(self, token, allow_expired=False):
|
|
# NOTE(jamielennox): allow_expired is ignored on V2
|
|
auth_ref = self._client.tokens.validate_access_info(token)
|
|
|
|
if not auth_ref:
|
|
msg = _('Failed to fetch token data from identity server')
|
|
raise ksm_exceptions.InvalidToken(msg)
|
|
|
|
return {'access': auth_ref}
|
|
|
|
|
|
class _V3RequestStrategy(_RequestStrategy):
|
|
|
|
AUTH_VERSION = (3, 0)
|
|
|
|
def __init__(self, adap, **kwargs):
|
|
super(_V3RequestStrategy, self).__init__(adap, **kwargs)
|
|
client_args = {'session': adap}
|
|
if self._requested_auth_interface:
|
|
client_args['interface'] = self._requested_auth_interface
|
|
self._client = v3_client.Client(**client_args)
|
|
|
|
def verify_token(self, token, allow_expired=False):
|
|
auth_ref = self._client.tokens.validate(
|
|
token,
|
|
include_catalog=self._include_service_catalog,
|
|
allow_expired=allow_expired,
|
|
access_rules_support=ACCESS_RULES_SUPPORT)
|
|
|
|
if not auth_ref:
|
|
msg = _('Failed to fetch token data from identity server')
|
|
raise ksm_exceptions.InvalidToken(msg)
|
|
|
|
return {'token': auth_ref}
|
|
|
|
|
|
_REQUEST_STRATEGIES = [_V3RequestStrategy, _V2RequestStrategy]
|
|
|
|
|
|
class IdentityServer(object):
|
|
"""Base class for operations on the Identity API server.
|
|
|
|
The auth_token middleware needs to communicate with the Identity API server
|
|
to validate tokens. This class encapsulates the data and methods to perform
|
|
the operations.
|
|
|
|
"""
|
|
|
|
def __init__(self, log, adap, include_service_catalog=None,
|
|
requested_auth_version=None, requested_auth_interface=None):
|
|
self._LOG = log
|
|
self._adapter = adap
|
|
self._include_service_catalog = include_service_catalog
|
|
self._requested_auth_version = requested_auth_version
|
|
self._requested_auth_interface = requested_auth_interface
|
|
|
|
# Built on-demand with self._request_strategy.
|
|
self._request_strategy_obj = None
|
|
|
|
@property
|
|
def www_authenticate_uri(self):
|
|
www_authenticate_uri = self._adapter.get_endpoint(
|
|
interface=plugin.AUTH_INTERFACE)
|
|
|
|
# NOTE(jamielennox): This weird stripping of the prefix hack is
|
|
# only relevant to the legacy case. We urljoin '/' to get just the
|
|
# base URI as this is the original behaviour.
|
|
if isinstance(self._adapter.auth, _auth.AuthTokenPlugin):
|
|
www_authenticate_uri = urllib.parse.urljoin(
|
|
www_authenticate_uri, '/').rstrip('/')
|
|
|
|
return www_authenticate_uri
|
|
|
|
@property
|
|
def auth_version(self):
|
|
return self._request_strategy.AUTH_VERSION
|
|
|
|
@property
|
|
def _request_strategy(self):
|
|
if not self._request_strategy_obj:
|
|
strategy_class = self._get_strategy_class()
|
|
self._adapter.version = strategy_class.AUTH_VERSION
|
|
|
|
self._request_strategy_obj = strategy_class(
|
|
self._adapter,
|
|
include_service_catalog=self._include_service_catalog,
|
|
requested_auth_interface=self._requested_auth_interface)
|
|
|
|
return self._request_strategy_obj
|
|
|
|
def _get_strategy_class(self):
|
|
if self._requested_auth_version:
|
|
# A specific version was requested.
|
|
if discover.version_match(_V3RequestStrategy.AUTH_VERSION,
|
|
self._requested_auth_version):
|
|
return _V3RequestStrategy
|
|
|
|
# The version isn't v3 so we don't know what to do. Just assume V2.
|
|
return _V2RequestStrategy
|
|
|
|
# Specific version was not requested then we fall through to
|
|
# discovering available versions from the server
|
|
for klass in _REQUEST_STRATEGIES:
|
|
if self._adapter.get_endpoint(version=klass.AUTH_VERSION):
|
|
self._LOG.debug('Auth Token confirmed use of %s apis',
|
|
klass.AUTH_VERSION)
|
|
return klass
|
|
|
|
versions = ['v%d.%d' % s.AUTH_VERSION for s in _REQUEST_STRATEGIES]
|
|
self._LOG.error('No attempted versions [%s] supported by server',
|
|
', '.join(versions))
|
|
|
|
msg = _('No compatible apis supported by server')
|
|
raise ksm_exceptions.ServiceError(msg)
|
|
|
|
def verify_token(self, user_token, retry=True, allow_expired=False):
|
|
"""Authenticate user token with identity server.
|
|
|
|
:param user_token: user's token id
|
|
:param retry: flag that forces the middleware to retry
|
|
user authentication when an indeterminate
|
|
response is received. Optional.
|
|
:param allow_expired: Allow retrieving an expired token.
|
|
:returns: access info received from identity server on success
|
|
:rtype: :py:class:`keystoneauth1.access.AccessInfo`
|
|
:raises exc.InvalidToken: if token is rejected
|
|
:raises exc.ServiceError: if unable to authenticate token
|
|
|
|
"""
|
|
try:
|
|
auth_ref = self._request_strategy.verify_token(
|
|
user_token,
|
|
allow_expired=allow_expired)
|
|
except ksa_exceptions.NotFound as e:
|
|
self._LOG.info('Authorization failed for token')
|
|
self._LOG.info('Identity response: %s', e.response.text)
|
|
raise ksm_exceptions.InvalidToken(_('Token authorization failed'))
|
|
except ksa_exceptions.Unauthorized as e:
|
|
self._LOG.info('Identity server rejected authorization')
|
|
self._LOG.warning('Identity response: %s', e.response.text)
|
|
if retry:
|
|
self._LOG.info('Retrying validation')
|
|
return self.verify_token(user_token, False)
|
|
msg = _('Identity server rejected authorization necessary to '
|
|
'fetch token data')
|
|
raise ksm_exceptions.ServiceError(msg)
|
|
except ksa_exceptions.HttpError as e:
|
|
self._LOG.error(
|
|
'Bad response code while validating token: %s %s',
|
|
e.http_status, e.message)
|
|
if hasattr(e.response, 'text'):
|
|
self._LOG.warning('Identity response: %s', e.response.text)
|
|
msg = _('Failed to fetch token data from identity server')
|
|
raise ksm_exceptions.ServiceError(msg)
|
|
else:
|
|
return auth_ref
|
|
|
|
def invalidate(self):
|
|
return self._adapter.invalidate()
|