diff options
-rw-r--r-- | doc/source/middlewarearchitecture.rst | 4 | ||||
-rw-r--r-- | keystoneclient/middleware/auth_token.py | 129 | ||||
-rw-r--r-- | tests/test_auth_token_middleware.py | 2 |
3 files changed, 66 insertions, 69 deletions
diff --git a/doc/source/middlewarearchitecture.rst b/doc/source/middlewarearchitecture.rst index f5fbae7..03782fa 100644 --- a/doc/source/middlewarearchitecture.rst +++ b/doc/source/middlewarearchitecture.rst @@ -193,6 +193,10 @@ Configuration Options * ``certfile``: (required, if Keystone server requires client cert) * ``keyfile``: (required, if Keystone server requires client cert) This can be the same as the certfile if the certfile includes the private key. +* ``cafile``: (optional, defaults to use system CA bundle) the path to a PEM + encoded CA file/bundle that will be used to verify HTTPS connections. +* ``insecure``: (optional, default `False`) Don't verify HTTPS connections + (overrides `cafile`). Caching for improved response ----------------------------- diff --git a/keystoneclient/middleware/auth_token.py b/keystoneclient/middleware/auth_token.py index 769b61b..1dc0a7b 100644 --- a/keystoneclient/middleware/auth_token.py +++ b/keystoneclient/middleware/auth_token.py @@ -145,9 +145,9 @@ keystone.token_info """ import datetime -import httplib import logging import os +import requests import stat import tempfile import time @@ -259,6 +259,10 @@ opts = [ help='Required if Keystone server requires client certificate'), cfg.StrOpt('keyfile', help='Required if Keystone server requires client certificate'), + cfg.StrOpt('cafile', default=None, + help='A PEM encoded Certificate Authority to use when ' + 'verifying HTTPs connections. Defaults to system CAs.'), + cfg.BoolOpt('insecure', default=False, help='Verify HTTPS connections.'), cfg.StrOpt('signing_dir', help='Directory used to cache files related to PKI tokens'), cfg.ListOpt('memcached_servers', @@ -354,43 +358,35 @@ class AuthProtocol(object): (True, 'true', 't', '1', 'on', 'yes', 'y')) # where to find the auth service (we use this to validate tokens) - self.auth_host = self._conf_get('auth_host') - self.auth_port = int(self._conf_get('auth_port')) - self.auth_protocol = self._conf_get('auth_protocol') - if not self._conf_get('http_handler'): - if self.auth_protocol == 'http': - self.http_client_class = httplib.HTTPConnection - else: - self.http_client_class = httplib.HTTPSConnection - else: - # Really only used for unit testing, since we need to - # have a fake handler set up before we issue an http - # request to get the list of versions supported by the - # server at the end of this initialization - self.http_client_class = self._conf_get('http_handler') - + auth_host = self._conf_get('auth_host') + auth_port = int(self._conf_get('auth_port')) + auth_protocol = self._conf_get('auth_protocol') self.auth_admin_prefix = self._conf_get('auth_admin_prefix') self.auth_uri = self._conf_get('auth_uri') + + if netaddr.valid_ipv6(auth_host): + # Note(dzyu) it is an IPv6 address, so it needs to be wrapped + # with '[]' to generate a valid IPv6 URL, based on + # http://www.ietf.org/rfc/rfc2732.txt + auth_host = '[%s]' % auth_host + + self.request_uri = '%s://%s:%s' % (auth_protocol, auth_host, auth_port) + if self.auth_uri is None: self.LOG.warning( 'Configuring auth_uri to point to the public identity ' 'endpoint is required; clients may not be able to ' 'authenticate against an admin endpoint') - host = self.auth_host - if netaddr.valid_ipv6(host): - # Note(dzyu) it is an IPv6 address, so it needs to be wrapped - # with '[]' to generate a valid IPv6 URL, based on - # http://www.ietf.org/rfc/rfc2732.txt - host = '[%s]' % host + # FIXME(dolph): drop support for this fallback behavior as # documented in bug 1207517 - self.auth_uri = '%s://%s:%s' % (self.auth_protocol, - host, - self.auth_port) + self.auth_uri = self.request_uri # SSL self.cert_file = self._conf_get('certfile') self.key_file = self._conf_get('keyfile') + self.ssl_ca_file = self._conf_get('cafile') + self.ssl_insecure = self._conf_get('insecure') # signing self.signing_dirname = self._conf_get('signing_dir') @@ -403,7 +399,7 @@ class AuthProtocol(object): val = '%s/signing_cert.pem' % self.signing_dirname self.signing_cert_file_name = val val = '%s/cacert.pem' % self.signing_dirname - self.ca_file_name = val + self.signing_ca_file_name = val val = '%s/revoked.pem' % self.signing_dirname self.revoked_file_name = val @@ -505,12 +501,12 @@ class AuthProtocol(object): def _get_supported_versions(self): versions = [] response, data = self._json_request('GET', '/') - if response.status == 501: + if response.status_code == 501: self.LOG.warning("Old keystone installation found...assuming v2.0") versions.append("v2.0") - elif response.status != 300: + elif response.status_code != 300: self.LOG.error('Unable to get version info from keystone: %s' % - response.status) + response.status_code) raise ServiceError('Unable to get version info from keystone') else: try: @@ -648,17 +644,6 @@ class AuthProtocol(object): return self.admin_token - def _get_http_connection(self): - if self.auth_protocol == 'http': - return self.http_client_class(self.auth_host, self.auth_port, - timeout=self.http_connect_timeout) - else: - return self.http_client_class(self.auth_host, - self.auth_port, - self.key_file, - self.cert_file, - timeout=self.http_connect_timeout) - def _http_request(self, method, path, **kwargs): """HTTP request helper used to make unspecified content type requests. @@ -668,28 +653,35 @@ class AuthProtocol(object): :raise ServerError when unable to communicate with keystone """ - conn = self._get_http_connection() + url = "%s/%s" % (self.request_uri, path.lstrip('/')) + + kwargs.setdefault('timeout', self.http_connect_timeout) + if self.cert_file and self.key_file: + kwargs['cert'] = (self.cert_file, self.key_file) + elif self.cert_file or self.key_file: + self.LOG.warn('Cannot use only a cert or key file. ' + 'Please provide both. Ignoring.') + + kwargs['verify'] = self.ssl_ca_file or True + if self.ssl_insecure: + kwargs['verify'] = False RETRIES = self.http_request_max_retries retry = 0 while True: try: - conn.request(method, path, **kwargs) - response = conn.getresponse() - body = response.read() + response = requests.request(method, url, **kwargs) break except Exception as e: - if retry == RETRIES: - self.LOG.error('HTTP connection exception: %s' % e) + if retry >= RETRIES: + self.LOG.error('HTTP connection exception: %s', e) raise NetworkError('Unable to communicate with keystone') # NOTE(vish): sleep 0.5, 1, 2 self.LOG.warn('Retrying on HTTP connection exception: %s' % e) time.sleep(2.0 ** retry / 2) retry += 1 - finally: - conn.close() - return response, body + return response def _json_request(self, method, path, body=None, additional_headers=None): """HTTP request helper used to make json requests. @@ -714,14 +706,14 @@ class AuthProtocol(object): kwargs['headers'].update(additional_headers) if body: - kwargs['body'] = jsonutils.dumps(body) + kwargs['data'] = jsonutils.dumps(body) path = self.auth_admin_prefix + path - response, body = self._http_request(method, path, **kwargs) + response = self._http_request(method, path, **kwargs) try: - data = jsonutils.loads(body) + data = jsonutils.loads(response.text) except ValueError: self.LOG.debug('Keystone did not return json-encoded body') data = {} @@ -1090,18 +1082,18 @@ class AuthProtocol(object): '/v2.0/tokens/%s' % safe_quote(user_token), additional_headers=headers) - if response.status == 200: + if response.status_code == 200: return data - if response.status == 404: + if response.status_code == 404: self.LOG.warn("Authorization failed for token %s", user_token) raise InvalidUserToken('Token authorization failed') - if response.status == 401: + if response.status_code == 401: self.LOG.info( 'Keystone rejected admin token %s, resetting', headers) self.admin_token = None else: self.LOG.error('Bad response code while validating token: %s' % - response.status) + response.status_code) if retry: self.LOG.info('Retrying validation') return self._validate_user_token(user_token, False) @@ -1135,13 +1127,14 @@ class AuthProtocol(object): while True: try: output = cms.cms_verify(data, self.signing_cert_file_name, - self.ca_file_name) + self.signing_ca_file_name) except cms.subprocess.CalledProcessError as err: if self.cert_file_missing(err.output, self.signing_cert_file_name): self.fetch_signing_cert() continue - if self.cert_file_missing(err.output, self.ca_file_name): + if self.cert_file_missing(err.output, + self.signing_ca_file_name): self.fetch_ca_cert() continue self.LOG.warning('Verify error: %s' % err) @@ -1221,14 +1214,14 @@ class AuthProtocol(object): headers = {'X-Auth-Token': self.get_admin_token()} response, data = self._json_request('GET', '/v2.0/tokens/revoked', additional_headers=headers) - if response.status == 401: + if response.status_code == 401: if retry: self.LOG.info( 'Keystone rejected admin token %s, resetting admin token', headers) self.admin_token = None return self.fetch_revocation_list(retry=False) - if response.status != 200: + if response.status_code != 200: raise ServiceError('Unable to fetch token revocation list.') if 'signed' not in data: raise ServiceError('Revocation list improperly formatted.') @@ -1237,7 +1230,7 @@ class AuthProtocol(object): def fetch_signing_cert(self): path = self.auth_admin_prefix.rstrip('/') path += '/v2.0/certificates/signing' - response, data = self._http_request('GET', path) + response = self._http_request('GET', path) def write_cert_file(data): with open(self.signing_cert_file_name, 'w') as certfile: @@ -1246,26 +1239,26 @@ class AuthProtocol(object): try: #todo check response try: - write_cert_file(data) + write_cert_file(response.text) except IOError: self.verify_signing_dir() - write_cert_file(data) + write_cert_file(response.text) except (AssertionError, KeyError): self.LOG.warn( - "Unexpected response from keystone service: %s", data) + "Unexpected response from keystone service: %s", response.text) raise ServiceError('invalid json response') def fetch_ca_cert(self): path = self.auth_admin_prefix.rstrip('/') + '/v2.0/certificates/ca' - response, data = self._http_request('GET', path) + response = self._http_request('GET', path) try: #todo check response - with open(self.ca_file_name, 'w') as certfile: - certfile.write(data) + with open(self.signing_ca_file_name, 'w') as certfile: + certfile.write(response.text) except (AssertionError, KeyError): self.LOG.warn( - "Unexpected response from keystone service: %s", data) + "Unexpected response from keystone service: %s", response.text) raise ServiceError('invalid json response') diff --git a/tests/test_auth_token_middleware.py b/tests/test_auth_token_middleware.py index 85037d5..17eacb6 100644 --- a/tests/test_auth_token_middleware.py +++ b/tests/test_auth_token_middleware.py @@ -864,7 +864,7 @@ class CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest): body=data) self.middleware.fetch_ca_cert() - with open(self.middleware.ca_file_name, 'r') as f: + with open(self.middleware.signing_ca_file_name, 'r') as f: self.assertEqual(f.read(), data) self.assertEqual("/testadmin/v2.0/certificates/ca", |