summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamie Lennox <jamielennox@redhat.com>2015-02-19 16:50:05 +1100
committerJamie Lennox <jamielennox@redhat.com>2015-02-26 10:01:15 +1100
commit1272e7ca045657cd9526e63b8a30fd577a6e6d34 (patch)
treea8bc6d3f540ad0a91380df93808c5c7f53927219
parent9a511ee24e3a9b21122f4ec1b20eee8b65801bda (diff)
downloadkeystonemiddleware-1272e7ca045657cd9526e63b8a30fd577a6e6d34.tar.gz
Extract IdentityServer into file
Extract the IdentityServer class and its helpers into their own file. As part of this I extracted a few small functions into the _utils file as they didn't really belong with IdentitServer. Change-Id: I16bff5200c5687f364e7ec2cc87ba8fc8aaab277 Implements: bp refactor-extract-module
-rw-r--r--keystonemiddleware/auth_token/__init__.py251
-rw-r--r--keystonemiddleware/auth_token/_identity.py243
-rw-r--r--keystonemiddleware/auth_token/_utils.py32
-rw-r--r--keystonemiddleware/tests/auth_token/test_auth_token_middleware.py21
-rw-r--r--keystonemiddleware/tests/auth_token/test_utils.py37
5 files changed, 318 insertions, 266 deletions
diff --git a/keystonemiddleware/auth_token/__init__.py b/keystonemiddleware/auth_token/__init__.py
index d194137..09445f7 100644
--- a/keystonemiddleware/auth_token/__init__.py
+++ b/keystonemiddleware/auth_token/__init__.py
@@ -186,14 +186,15 @@ from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import timeutils
import six
-from six.moves import urllib
from keystonemiddleware.auth_token import _auth
from keystonemiddleware.auth_token import _base
from keystonemiddleware.auth_token import _cache
from keystonemiddleware.auth_token import _exceptions as exc
+from keystonemiddleware.auth_token import _identity
from keystonemiddleware.auth_token import _revocations
from keystonemiddleware.auth_token import _signing_dir
+from keystonemiddleware.auth_token import _utils
from keystonemiddleware.i18n import _, _LC, _LE, _LI, _LW
@@ -445,11 +446,6 @@ def _v3_to_v2_catalog(catalog):
return v2_services
-def _safe_quote(s):
- """URL-encode strings that are not already URL-encoded."""
- return urllib.parse.quote(s) if s == urllib.parse.unquote(s) else s
-
-
def _conf_values_type_convert(conf):
"""Convert conf values into correct type."""
if not conf:
@@ -481,19 +477,6 @@ def _conf_values_type_convert(conf):
return opts
-class _MiniResp(object):
- def __init__(self, error_message, env, headers=[]):
- # The HEAD method is unique: it must never return a body, even if
- # it reports an error (RFC-2616 clause 9.4). We relieve callers
- # from varying the error responses depending on the method.
- if env['REQUEST_METHOD'] == 'HEAD':
- self.body = ['']
- else:
- self.body = [error_message.encode()]
- self.headers = list(headers)
- self.headers.append(('Content-type', 'text/plain'))
-
-
class _TokenData(object):
"""An abstraction to show auth_token consumers some of the token contents.
@@ -797,7 +780,7 @@ class AuthProtocol(object):
return self._call_app(env, start_response)
def _do_503_error(self, env, start_response):
- resp = _MiniResp('Service unavailable', env)
+ resp = _utils.MiniResp('Service unavailable', env)
start_response('503 Service Unavailable', resp.headers)
return resp.body
@@ -875,8 +858,8 @@ class AuthProtocol(object):
:returns: HTTPUnauthorized http response
"""
- resp = _MiniResp('Authentication required',
- env, self._reject_auth_headers)
+ resp = _utils.MiniResp('Authentication required',
+ env, self._reject_auth_headers)
start_response('401 Unauthorized', resp.headers)
return resp.body
@@ -1225,7 +1208,7 @@ class AuthProtocol(object):
auth_version = self._conf_get('auth_version')
if auth_version is not None:
auth_version = discover.normalize_version_number(auth_version)
- return _IdentityServer(
+ return _identity.IdentityServer(
self._LOG,
adap,
include_service_catalog=self._include_service_catalog,
@@ -1261,228 +1244,6 @@ class AuthProtocol(object):
return _cache.TokenCache(self._LOG, **cache_kwargs)
-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 UUID tokens, fetch the revocation list, signing certificates,
- etc. This class encapsulates the data and methods to perform these
- operations.
-
- """
-
- def __init__(self, log, adap, include_service_catalog=None,
- requested_auth_version=None):
- self._LOG = log
- self._adapter = adap
- self._include_service_catalog = include_service_catalog
- self._requested_auth_version = requested_auth_version
-
- # Built on-demand with self._request_strategy.
- self._request_strategy_obj = None
-
- @property
- def auth_uri(self):
- auth_uri = self._adapter.get_endpoint(interface=auth.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):
- auth_uri = urllib.parse.urljoin(auth_uri, '/').rstrip('/')
-
- return auth_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._json_request,
- self._adapter,
- include_service_catalog=self._include_service_catalog)
-
- 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):
- msg = _LI('Auth Token confirmed use of %s apis')
- self._LOG.info(msg, self._requested_auth_version)
- return klass
-
- versions = ['v%d.%d' % s.AUTH_VERSION for s in _REQUEST_STRATEGIES]
- self._LOG.error(_LE('No attempted versions [%s] supported by server'),
- ', '.join(versions))
-
- msg = _('No compatible apis supported by server')
- raise exc.ServiceError(msg)
-
- def verify_token(self, user_token, retry=True):
- """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.
- :returns: token object received from identity server on success
- :raises exc.InvalidToken: if token is rejected
- :raises exc.ServiceError: if unable to authenticate token
-
- """
- user_token = _safe_quote(user_token)
-
- try:
- response, data = self._request_strategy.verify_token(user_token)
- except exceptions.NotFound as e:
- self._LOG.warn(_LW('Authorization failed for token'))
- self._LOG.warn(_LW('Identity response: %s'), e.response.text)
- except exceptions.Unauthorized as e:
- self._LOG.info(_LI('Identity server rejected authorization'))
- self._LOG.warn(_LW('Identity response: %s'), e.response.text)
- if retry:
- self._LOG.info(_LI('Retrying validation'))
- return self.verify_token(user_token, False)
- except exceptions.HttpError as e:
- self._LOG.error(
- _LE('Bad response code while validating token: %s'),
- e.http_status)
- self._LOG.warn(_LW('Identity response: %s'), e.response.text)
- else:
- if response.status_code == 200:
- return data
-
- raise exc.InvalidToken()
-
- def fetch_revocation_list(self):
- try:
- response, data = self._json_request(
- 'GET', '/tokens/revoked',
- authenticated=True,
- endpoint_filter={'version': (2, 0)})
- except exceptions.HTTPError as e:
- msg = _('Failed to fetch token revocation list: %d')
- raise exc.RevocationListError(msg % e.http_status)
- if response.status_code != 200:
- msg = _('Unable to fetch token revocation list.')
- raise exc.RevocationListError(msg)
- if 'signed' not in data:
- msg = _('Revocation list improperly formatted.')
- raise exc.RevocationListError(msg)
- return data['signed']
-
- def fetch_signing_cert(self):
- return self._fetch_cert_file('signing')
-
- def fetch_ca_cert(self):
- return self._fetch_cert_file('ca')
-
- def _json_request(self, method, path, **kwargs):
- """HTTP request helper used to make json requests.
-
- :param method: http method
- :param path: relative request url
- :param **kwargs: additional parameters used by session or endpoint
- :returns: http response object, response body parsed as json
- :raises ServerError: when unable to communicate with identity server.
-
- """
- headers = kwargs.setdefault('headers', {})
- headers['Accept'] = 'application/json'
-
- response = self._adapter.request(path, method, **kwargs)
-
- try:
- data = jsonutils.loads(response.text)
- except ValueError:
- self._LOG.debug('Identity server did not return json-encoded body')
- data = {}
-
- return response, data
-
- def _fetch_cert_file(self, cert_type):
- try:
- response = self._request_strategy.fetch_cert_file(cert_type)
- except exceptions.HTTPError as e:
- raise exceptions.CertificateConfigError(e.details)
- if response.status_code != 200:
- raise exceptions.CertificateConfigError(response.text)
- return response.text
-
-
-class _RequestStrategy(object):
-
- AUTH_VERSION = None
-
- def __init__(self, json_request, adap, include_service_catalog=None):
- self._json_request = json_request
- self._adapter = adap
- self._include_service_catalog = include_service_catalog
-
- def verify_token(self, user_token):
- pass
-
- def fetch_cert_file(self, cert_type):
- pass
-
-
-class _V2RequestStrategy(_RequestStrategy):
-
- AUTH_VERSION = (2, 0)
-
- def verify_token(self, user_token):
- return self._json_request('GET',
- '/tokens/%s' % user_token,
- authenticated=True)
-
- def fetch_cert_file(self, cert_type):
- return self._adapter.get('/certificates/%s' % cert_type,
- authenticated=False)
-
-
-class _V3RequestStrategy(_RequestStrategy):
-
- AUTH_VERSION = (3, 0)
-
- def verify_token(self, user_token):
- path = '/auth/tokens'
- if not self._include_service_catalog:
- path += '?nocatalog'
-
- return self._json_request('GET',
- path,
- authenticated=True,
- headers={'X-Subject-Token': user_token})
-
- def fetch_cert_file(self, cert_type):
- if cert_type == 'signing':
- cert_type = 'certificates'
-
- return self._adapter.get('/OS-SIMPLE-CERT/%s' % cert_type,
- authenticated=False)
-
-
-# NOTE(jamielennox): must be defined after request strategy classes
-_REQUEST_STRATEGIES = [_V3RequestStrategy, _V2RequestStrategy]
-
-
def filter_factory(global_conf, **local_conf):
"""Returns a WSGI filter app for use with paste.deploy."""
conf = global_conf.copy()
diff --git a/keystonemiddleware/auth_token/_identity.py b/keystonemiddleware/auth_token/_identity.py
new file mode 100644
index 0000000..8acf70d
--- /dev/null
+++ b/keystonemiddleware/auth_token/_identity.py
@@ -0,0 +1,243 @@
+# 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 keystoneclient import auth
+from keystoneclient import discover
+from keystoneclient import exceptions
+from oslo_serialization import jsonutils
+from six.moves import urllib
+
+from keystonemiddleware.auth_token import _auth
+from keystonemiddleware.auth_token import _exceptions as exc
+from keystonemiddleware.auth_token import _utils
+from keystonemiddleware.i18n import _, _LE, _LI, _LW
+
+
+class _RequestStrategy(object):
+
+ AUTH_VERSION = None
+
+ def __init__(self, json_request, adap, include_service_catalog=None):
+ self._json_request = json_request
+ self._adapter = adap
+ self._include_service_catalog = include_service_catalog
+
+ def verify_token(self, user_token):
+ pass
+
+ def fetch_cert_file(self, cert_type):
+ pass
+
+
+class _V2RequestStrategy(_RequestStrategy):
+
+ AUTH_VERSION = (2, 0)
+
+ def verify_token(self, user_token):
+ return self._json_request('GET',
+ '/tokens/%s' % user_token,
+ authenticated=True)
+
+ def fetch_cert_file(self, cert_type):
+ return self._adapter.get('/certificates/%s' % cert_type,
+ authenticated=False)
+
+
+class _V3RequestStrategy(_RequestStrategy):
+
+ AUTH_VERSION = (3, 0)
+
+ def verify_token(self, user_token):
+ path = '/auth/tokens'
+ if not self._include_service_catalog:
+ path += '?nocatalog'
+
+ return self._json_request('GET',
+ path,
+ authenticated=True,
+ headers={'X-Subject-Token': user_token})
+
+ def fetch_cert_file(self, cert_type):
+ if cert_type == 'signing':
+ cert_type = 'certificates'
+
+ return self._adapter.get('/OS-SIMPLE-CERT/%s' % cert_type,
+ authenticated=False)
+
+
+_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 UUID tokens, fetch the revocation list, signing certificates,
+ etc. This class encapsulates the data and methods to perform these
+ operations.
+
+ """
+
+ def __init__(self, log, adap, include_service_catalog=None,
+ requested_auth_version=None):
+ self._LOG = log
+ self._adapter = adap
+ self._include_service_catalog = include_service_catalog
+ self._requested_auth_version = requested_auth_version
+
+ # Built on-demand with self._request_strategy.
+ self._request_strategy_obj = None
+
+ @property
+ def auth_uri(self):
+ auth_uri = self._adapter.get_endpoint(interface=auth.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):
+ auth_uri = urllib.parse.urljoin(auth_uri, '/').rstrip('/')
+
+ return auth_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._json_request,
+ self._adapter,
+ include_service_catalog=self._include_service_catalog)
+
+ 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):
+ msg = _LI('Auth Token confirmed use of %s apis')
+ self._LOG.info(msg, self._requested_auth_version)
+ return klass
+
+ versions = ['v%d.%d' % s.AUTH_VERSION for s in _REQUEST_STRATEGIES]
+ self._LOG.error(_LE('No attempted versions [%s] supported by server'),
+ ', '.join(versions))
+
+ msg = _('No compatible apis supported by server')
+ raise exc.ServiceError(msg)
+
+ def verify_token(self, user_token, retry=True):
+ """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.
+ :returns: token object received from identity server on success
+ :raises exc.InvalidToken: if token is rejected
+ :raises exc.ServiceError: if unable to authenticate token
+
+ """
+ user_token = _utils.safe_quote(user_token)
+
+ try:
+ response, data = self._request_strategy.verify_token(user_token)
+ except exceptions.NotFound as e:
+ self._LOG.warn(_LW('Authorization failed for token'))
+ self._LOG.warn(_LW('Identity response: %s'), e.response.text)
+ except exceptions.Unauthorized as e:
+ self._LOG.info(_LI('Identity server rejected authorization'))
+ self._LOG.warn(_LW('Identity response: %s'), e.response.text)
+ if retry:
+ self._LOG.info(_LI('Retrying validation'))
+ return self.verify_token(user_token, False)
+ except exceptions.HttpError as e:
+ self._LOG.error(
+ _LE('Bad response code while validating token: %s'),
+ e.http_status)
+ self._LOG.warn(_LW('Identity response: %s'), e.response.text)
+ else:
+ if response.status_code == 200:
+ return data
+
+ raise exc.InvalidToken()
+
+ def fetch_revocation_list(self):
+ try:
+ response, data = self._json_request(
+ 'GET', '/tokens/revoked',
+ authenticated=True,
+ endpoint_filter={'version': (2, 0)})
+ except exceptions.HTTPError as e:
+ msg = _('Failed to fetch token revocation list: %d')
+ raise exc.RevocationListError(msg % e.http_status)
+ if response.status_code != 200:
+ msg = _('Unable to fetch token revocation list.')
+ raise exc.RevocationListError(msg)
+ if 'signed' not in data:
+ msg = _('Revocation list improperly formatted.')
+ raise exc.RevocationListError(msg)
+ return data['signed']
+
+ def fetch_signing_cert(self):
+ return self._fetch_cert_file('signing')
+
+ def fetch_ca_cert(self):
+ return self._fetch_cert_file('ca')
+
+ def _json_request(self, method, path, **kwargs):
+ """HTTP request helper used to make json requests.
+
+ :param method: http method
+ :param path: relative request url
+ :param **kwargs: additional parameters used by session or endpoint
+ :returns: http response object, response body parsed as json
+ :raises ServerError: when unable to communicate with identity server.
+
+ """
+ headers = kwargs.setdefault('headers', {})
+ headers['Accept'] = 'application/json'
+
+ response = self._adapter.request(path, method, **kwargs)
+
+ try:
+ data = jsonutils.loads(response.text)
+ except ValueError:
+ self._LOG.debug('Identity server did not return json-encoded body')
+ data = {}
+
+ return response, data
+
+ def _fetch_cert_file(self, cert_type):
+ try:
+ response = self._request_strategy.fetch_cert_file(cert_type)
+ except exceptions.HTTPError as e:
+ raise exceptions.CertificateConfigError(e.details)
+ if response.status_code != 200:
+ raise exceptions.CertificateConfigError(response.text)
+ return response.text
diff --git a/keystonemiddleware/auth_token/_utils.py b/keystonemiddleware/auth_token/_utils.py
new file mode 100644
index 0000000..daed02d
--- /dev/null
+++ b/keystonemiddleware/auth_token/_utils.py
@@ -0,0 +1,32 @@
+# 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 six.moves import urllib
+
+
+def safe_quote(s):
+ """URL-encode strings that are not already URL-encoded."""
+ return urllib.parse.quote(s) if s == urllib.parse.unquote(s) else s
+
+
+class MiniResp(object):
+
+ def __init__(self, error_message, env, headers=[]):
+ # The HEAD method is unique: it must never return a body, even if
+ # it reports an error (RFC-2616 clause 9.4). We relieve callers
+ # from varying the error responses depending on the method.
+ if env['REQUEST_METHOD'] == 'HEAD':
+ self.body = ['']
+ else:
+ self.body = [error_message.encode()]
+ self.headers = list(headers)
+ self.headers.append(('Content-type', 'text/plain'))
diff --git a/keystonemiddleware/tests/auth_token/test_auth_token_middleware.py b/keystonemiddleware/tests/auth_token/test_auth_token_middleware.py
index 403c37f..ae087f9 100644
--- a/keystonemiddleware/tests/auth_token/test_auth_token_middleware.py
+++ b/keystonemiddleware/tests/auth_token/test_auth_token_middleware.py
@@ -1908,27 +1908,6 @@ class v3AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
self.assertIsNone(t.trust_id)
-class TokenEncodingTest(testtools.TestCase):
- def test_unquoted_token(self):
- self.assertEqual('foo%20bar', auth_token._safe_quote('foo bar'))
-
- def test_quoted_token(self):
- self.assertEqual('foo%20bar', auth_token._safe_quote('foo%20bar'))
-
- def test_messages_encoded_as_bytes(self):
- """Test that string are passed around as bytes for PY3."""
- msg = "This is an error"
-
- class FakeResp(auth_token._MiniResp):
- def __init__(self, error, env):
- super(FakeResp, self).__init__(error, env)
-
- fake_resp = FakeResp(msg, dict(REQUEST_METHOD='GET'))
- # On Py2 .encode() don't do much but that's better than to
- # have a ifdef with six.PY3
- self.assertEqual(msg.encode(), fake_resp.body[0])
-
-
class TokenExpirationTest(BaseAuthTokenMiddlewareTest):
def setUp(self):
super(TokenExpirationTest, self).setUp()
diff --git a/keystonemiddleware/tests/auth_token/test_utils.py b/keystonemiddleware/tests/auth_token/test_utils.py
new file mode 100644
index 0000000..fcd1e62
--- /dev/null
+++ b/keystonemiddleware/tests/auth_token/test_utils.py
@@ -0,0 +1,37 @@
+# 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 testtools
+
+from keystonemiddleware.auth_token import _utils
+
+
+class TokenEncodingTest(testtools.TestCase):
+
+ def test_unquoted_token(self):
+ self.assertEqual('foo%20bar', _utils.safe_quote('foo bar'))
+
+ def test_quoted_token(self):
+ self.assertEqual('foo%20bar', _utils.safe_quote('foo%20bar'))
+
+ def test_messages_encoded_as_bytes(self):
+ """Test that string are passed around as bytes for PY3."""
+ msg = "This is an error"
+
+ class FakeResp(_utils.MiniResp):
+ def __init__(self, error, env):
+ super(FakeResp, self).__init__(error, env)
+
+ fake_resp = FakeResp(msg, dict(REQUEST_METHOD='GET'))
+ # On Py2 .encode() don't do much but that's better than to
+ # have a ifdef with six.PY3
+ self.assertEqual(msg.encode(), fake_resp.body[0])