diff options
Diffstat (limited to 'keystoneclient/middleware')
-rw-r--r-- | keystoneclient/middleware/__init__.py | 0 | ||||
-rw-r--r-- | keystoneclient/middleware/auth_token.py | 864 | ||||
-rw-r--r-- | keystoneclient/middleware/test.py | 67 |
3 files changed, 931 insertions, 0 deletions
diff --git a/keystoneclient/middleware/__init__.py b/keystoneclient/middleware/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/keystoneclient/middleware/__init__.py diff --git a/keystoneclient/middleware/auth_token.py b/keystoneclient/middleware/auth_token.py new file mode 100644 index 0000000..b6c2be0 --- /dev/null +++ b/keystoneclient/middleware/auth_token.py @@ -0,0 +1,864 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2012 OpenStack LLC +# +# 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. + +""" +TOKEN-BASED AUTH MIDDLEWARE + +This WSGI component: + +* Verifies that incoming client requests have valid tokens by validating + tokens with the auth service. +* Rejects unauthenticated requests UNLESS it is in 'delay_auth_decision' + mode, which means the final decision is delegated to the downstream WSGI + component (usually the OpenStack service) +* Collects and forwards identity information based on a valid token + such as user name, tenant, etc + +Refer to: http://keystone.openstack.org/middlewarearchitecture.html + +HEADERS +------- + +* Headers starting with HTTP\_ is a standard http header +* Headers starting with HTTP_X is an extended http header + +Coming in from initial call from client or customer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +HTTP_X_AUTH_TOKEN + The client token being passed in. + +HTTP_X_STORAGE_TOKEN + The client token being passed in (legacy Rackspace use) to support + swift/cloud files + +Used for communication between components +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +WWW-Authenticate + HTTP header returned to a user indicating which endpoint to use + to retrieve a new token + +What we add to the request for use by the OpenStack service +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +HTTP_X_IDENTITY_STATUS + 'Confirmed' or 'Invalid' + The underlying service will only see a value of 'Invalid' if the Middleware + is configured to run in 'delay_auth_decision' mode + +HTTP_X_TENANT_ID + Identity service managed unique identifier, string + +HTTP_X_TENANT_NAME + Unique tenant identifier, string + +HTTP_X_USER_ID + Identity-service managed unique identifier, string + +HTTP_X_USER_NAME + Unique user identifier, string + +HTTP_X_ROLES + Comma delimited list of case-sensitive Roles + +HTTP_X_SERVICE_CATALOG + json encoded keystone service catalog (optional). + +HTTP_X_TENANT + *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME + Keystone-assigned unique identifier, deprecated + +HTTP_X_USER + *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME + Unique user name, string + +HTTP_X_ROLE + *Deprecated* in favor of HTTP_X_ROLES + This is being renamed, and the new header contains the same data. + +OTHER ENVIRONMENT VARIABLES +--------------------------- + +keystone.token_info + Information about the token discovered in the process of + validation. This may include extended information returned by the + Keystone token validation call, as well as basic information about + the tenant and user. + +""" + +import datetime +import httplib +import json +import logging +import os +import stat +import time +import webob +import webob.exc + +from keystoneclient.openstack.common import jsonutils +from keystoneclient.common import cms +from keystoneclient import utils +from keystoneclient.openstack.common import timeutils + +CONF = None +try: + from openstack.common import cfg + CONF = cfg.CONF +except ImportError: + # cfg is not a library yet, try application copies + for app in 'nova', 'glance', 'quantum', 'cinder': + try: + cfg = __import__('%s.openstack.common.cfg' % app, + fromlist=['%s.openstack.common' % app]) + # test which application middleware is running in + if hasattr(cfg, 'CONF') and 'config_file' in cfg.CONF: + CONF = cfg.CONF + break + except ImportError: + pass +if not CONF: + from keystoneclient.openstack.common import cfg + CONF = cfg.CONF +LOG = logging.getLogger(__name__) + +# alternative middleware configuration in the main application's +# configuration file e.g. in nova.conf +# [keystone_authtoken] +# auth_host = 127.0.0.1 +# auth_port = 35357 +# auth_protocol = http +# admin_tenant_name = admin +# admin_user = admin +# admin_password = badpassword +opts = [ + cfg.StrOpt('auth_admin_prefix', default=''), + cfg.StrOpt('auth_host', default='127.0.0.1'), + cfg.IntOpt('auth_port', default=35357), + cfg.StrOpt('auth_protocol', default='https'), + cfg.StrOpt('auth_uri', default=None), + cfg.BoolOpt('delay_auth_decision', default=False), + cfg.StrOpt('admin_token'), + cfg.StrOpt('admin_user'), + cfg.StrOpt('admin_password'), + cfg.StrOpt('admin_tenant_name', default='admin'), + cfg.StrOpt('certfile'), + cfg.StrOpt('keyfile'), + cfg.StrOpt('signing_dir'), + cfg.ListOpt('memcache_servers'), + cfg.IntOpt('token_cache_time', default=300), +] +CONF.register_opts(opts, group='keystone_authtoken') + + +def will_expire_soon(expiry): + """ Determines if expiration is about to occur. + + :param expiry: a datetime of the expected expiration + :returns: boolean : true if expiration is within 30 seconds + """ + soon = (timeutils.utcnow() + datetime.timedelta(seconds=30)) + return expiry < soon + + +class InvalidUserToken(Exception): + pass + + +class ServiceError(Exception): + pass + + +class ConfigurationError(Exception): + pass + + +class AuthProtocol(object): + """Auth Middleware that handles authenticating client calls.""" + + def __init__(self, app, conf): + LOG.info('Starting keystone auth_token middleware') + self.conf = conf + self.app = app + + # delay_auth_decision means we still allow unauthenticated requests + # through and we let the downstream service make the final decision + self.delay_auth_decision = (self._conf_get('delay_auth_decision') in + (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 self.auth_protocol == 'http': + self.http_client_class = httplib.HTTPConnection + else: + self.http_client_class = httplib.HTTPSConnection + + self.auth_admin_prefix = self._conf_get('auth_admin_prefix') + self.auth_uri = self._conf_get('auth_uri') + if self.auth_uri is None: + self.auth_uri = '%s://%s:%s' % (self.auth_protocol, + self.auth_host, + self.auth_port) + + # SSL + self.cert_file = self._conf_get('certfile') + self.key_file = self._conf_get('keyfile') + + #signing + self.signing_dirname = self._conf_get('signing_dir') + if self.signing_dirname is None: + self.signing_dirname = '%s/keystone-signing' % os.environ['HOME'] + LOG.info('Using %s as cache directory for signing certificate' % + self.signing_dirname) + if (os.path.exists(self.signing_dirname) and + not os.access(self.signing_dirname, os.W_OK)): + raise ConfigurationError("unable to access signing dir %s" % + self.signing_dirname) + + if not os.path.exists(self.signing_dirname): + os.makedirs(self.signing_dirname) + #will throw IOError if it cannot change permissions + os.chmod(self.signing_dirname, stat.S_IRWXU) + + 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 + val = '%s/revoked.pem' % self.signing_dirname + self.revoked_file_name = val + + # Credentials used to verify this component with the Auth service since + # validating tokens is a privileged call + self.admin_token = self._conf_get('admin_token') + self.admin_token_expiry = None + self.admin_user = self._conf_get('admin_user') + self.admin_password = self._conf_get('admin_password') + self.admin_tenant_name = self._conf_get('admin_tenant_name') + + # Token caching via memcache + self._cache = None + self._iso8601 = None + memcache_servers = self._conf_get('memcache_servers') + # By default the token will be cached for 5 minutes + self.token_cache_time = int(self._conf_get('token_cache_time')) + self._token_revocation_list = None + self._token_revocation_list_fetched_time = None + cache_timeout = datetime.timedelta(seconds=0) + self.token_revocation_list_cache_timeout = cache_timeout + if memcache_servers: + try: + import memcache + import iso8601 + LOG.info('Using memcache for caching token') + self._cache = memcache.Client(memcache_servers.split(',')) + self._iso8601 = iso8601 + except ImportError as e: + LOG.warn('disabled caching due to missing libraries %s', e) + + def _conf_get(self, name): + # try config from paste-deploy first + if name in self.conf: + return self.conf[name] + else: + return CONF.keystone_authtoken[name] + + def __call__(self, env, start_response): + """Handle incoming request. + + Authenticate send downstream on success. Reject request if + we can't authenticate. + + """ + LOG.debug('Authenticating user token') + try: + self._remove_auth_headers(env) + user_token = self._get_user_token_from_header(env) + token_info = self._validate_user_token(user_token) + env['keystone.token_info'] = token_info + user_headers = self._build_user_headers(token_info) + self._add_headers(env, user_headers) + return self.app(env, start_response) + + except InvalidUserToken: + if self.delay_auth_decision: + LOG.info('Invalid user token - deferring reject downstream') + self._add_headers(env, {'X-Identity-Status': 'Invalid'}) + return self.app(env, start_response) + else: + LOG.info('Invalid user token - rejecting request') + return self._reject_request(env, start_response) + + except ServiceError as e: + LOG.critical('Unable to obtain admin token: %s' % e) + resp = webob.exc.HTTPServiceUnavailable() + return resp(env, start_response) + + def _remove_auth_headers(self, env): + """Remove headers so a user can't fake authentication. + + :param env: wsgi request environment + + """ + auth_headers = ( + 'X-Identity-Status', + 'X-Tenant-Id', + 'X-Tenant-Name', + 'X-User-Id', + 'X-User-Name', + 'X-Roles', + 'X-Service-Catalog', + # Deprecated + 'X-User', + 'X-Tenant', + 'X-Role', + ) + LOG.debug('Removing headers from request environment: %s' % + ','.join(auth_headers)) + self._remove_headers(env, auth_headers) + + def _get_user_token_from_header(self, env): + """Get token id from request. + + :param env: wsgi request environment + :return token id + :raises InvalidUserToken if no token is provided in request + + """ + token = self._get_header(env, 'X-Auth-Token', + self._get_header(env, 'X-Storage-Token')) + if token: + return token + else: + LOG.warn("Unable to find authentication token in headers: %s", env) + raise InvalidUserToken('Unable to find token in headers') + + def _reject_request(self, env, start_response): + """Redirect client to auth server. + + :param env: wsgi request environment + :param start_response: wsgi response callback + :returns HTTPUnauthorized http response + + """ + headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % self.auth_uri)] + resp = webob.exc.HTTPUnauthorized('Authentication required', headers) + return resp(env, start_response) + + def get_admin_token(self): + """Return admin token, possibly fetching a new one. + + if self.admin_token_expiry is set from fetching an admin token, check + it for expiration, and request a new token is the existing token + is about to expire. + + :return admin token id + :raise ServiceError when unable to retrieve token from keystone + + """ + if self.admin_token_expiry: + if will_expire_soon(self.admin_token_expiry): + self.admin_token = None + + if not self.admin_token: + (self.admin_token, + self.admin_token_expiry) = self._request_admin_token() + + 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) + else: + return self.http_client_class(self.auth_host, + self.auth_port, + self.key_file, + self.cert_file) + + def _http_request(self, method, path): + """HTTP request helper used to make unspecified content type requests. + + :param method: http method + :param path: relative request url + :return (http response object) + :raise ServerError when unable to communicate with keystone + + """ + conn = self._get_http_connection() + + try: + conn.request(method, path) + response = conn.getresponse() + body = response.read() + except Exception as e: + LOG.error('HTTP connection exception: %s' % e) + raise ServiceError('Unable to communicate with keystone') + finally: + conn.close() + + return response, body + + def _json_request(self, method, path, body=None, additional_headers=None): + """HTTP request helper used to make json requests. + + :param method: http method + :param path: relative request url + :param body: dict to encode to json as request body. Optional. + :param additional_headers: dict of additional headers to send with + http request. Optional. + :return (http response object, response body parsed as json) + :raise ServerError when unable to communicate with keystone + + """ + conn = self._get_http_connection() + + kwargs = { + 'headers': { + 'Content-type': 'application/json', + 'Accept': 'application/json', + }, + } + + if additional_headers: + kwargs['headers'].update(additional_headers) + + if body: + kwargs['body'] = jsonutils.dumps(body) + + full_path = self.auth_admin_prefix + path + try: + conn.request(method, full_path, **kwargs) + response = conn.getresponse() + body = response.read() + except Exception as e: + LOG.error('HTTP connection exception: %s' % e) + raise ServiceError('Unable to communicate with keystone') + finally: + conn.close() + + try: + data = jsonutils.loads(body) + except ValueError: + LOG.debug('Keystone did not return json-encoded body') + data = {} + + return response, data + + def _request_admin_token(self): + """Retrieve new token as admin user from keystone. + + :return token id upon success + :raises ServerError when unable to communicate with keystone + + """ + params = { + 'auth': { + 'passwordCredentials': { + 'username': self.admin_user, + 'password': self.admin_password, + }, + 'tenantName': self.admin_tenant_name, + } + } + + response, data = self._json_request('POST', + '/v2.0/tokens', + body=params) + + try: + token = data['access']['token']['id'] + expiry = data['access']['token']['expires'] + assert token + assert expiry + datetime_expiry = timeutils.parse_isotime(expiry) + return (token, timeutils.normalize_time(datetime_expiry)) + except (AssertionError, KeyError): + LOG.warn("Unexpected response from keystone service: %s", data) + raise ServiceError('invalid json response') + except (ValueError): + LOG.warn("Unable to parse expiration time from token: %s", data) + raise ServiceError('invalid json response') + + def _validate_user_token(self, user_token, retry=True): + """Authenticate user using PKI + + :param user_token: user's token id + :param retry: Ignored, as it is not longer relevant + :return uncrypted body of the token if the token is valid + :raise InvalidUserToken if token is rejected + :no longer raises ServiceError since it no longer makes RPC + + """ + try: + token_id = cms.cms_hash_token(user_token) + cached = self._cache_get(token_id) + if cached: + return cached + if cms.is_ans1_token(user_token): + verified = self.verify_signed_token(user_token) + data = json.loads(verified) + else: + data = self.verify_uuid_token(user_token, retry) + self._cache_put(token_id, data) + return data + except Exception as e: + LOG.debug('Token validation failure.', exc_info=True) + self._cache_store_invalid(user_token) + LOG.warn("Authorization failed for token %s", user_token) + raise InvalidUserToken('Token authorization failed') + + def _build_user_headers(self, token_info): + """Convert token object into headers. + + Build headers that represent authenticated user: + * X_IDENTITY_STATUS: Confirmed or Invalid + * X_TENANT_ID: id of tenant if tenant is present + * X_TENANT_NAME: name of tenant if tenant is present + * X_USER_ID: id of user + * X_USER_NAME: name of user + * X_ROLES: list of roles + * X_SERVICE_CATALOG: service catalog + + Additional (deprecated) headers include: + * X_USER: name of user + * X_TENANT: For legacy compatibility before we had ID and Name + * X_ROLE: list of roles + + :param token_info: token object returned by keystone on authentication + :raise InvalidUserToken when unable to parse token object + + """ + user = token_info['access']['user'] + token = token_info['access']['token'] + roles = ','.join([role['name'] for role in user.get('roles', [])]) + + def get_tenant_info(): + """Returns a (tenant_id, tenant_name) tuple from context.""" + def essex(): + """Essex puts the tenant ID and name on the token.""" + return (token['tenant']['id'], token['tenant']['name']) + + def pre_diablo(): + """Pre-diablo, Keystone only provided tenantId.""" + return (token['tenantId'], token['tenantId']) + + def default_tenant(): + """Assume the user's default tenant.""" + return (user['tenantId'], user['tenantName']) + + for method in [essex, pre_diablo, default_tenant]: + try: + return method() + except KeyError: + pass + + raise InvalidUserToken('Unable to determine tenancy.') + + tenant_id, tenant_name = get_tenant_info() + + user_id = user['id'] + user_name = user['name'] + + rval = { + 'X-Identity-Status': 'Confirmed', + 'X-Tenant-Id': tenant_id, + 'X-Tenant-Name': tenant_name, + 'X-User-Id': user_id, + 'X-User-Name': user_name, + 'X-Roles': roles, + # Deprecated + 'X-User': user_name, + 'X-Tenant': tenant_name, + 'X-Role': roles, + } + + try: + catalog = token_info['access']['serviceCatalog'] + rval['X-Service-Catalog'] = jsonutils.dumps(catalog) + except KeyError: + pass + + return rval + + def _header_to_env_var(self, key): + """Convert header to wsgi env variable. + + :param key: http header name (ex. 'X-Auth-Token') + :return wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN') + + """ + return 'HTTP_%s' % key.replace('-', '_').upper() + + def _add_headers(self, env, headers): + """Add http headers to environment.""" + for (k, v) in headers.iteritems(): + env_key = self._header_to_env_var(k) + env[env_key] = v + + def _remove_headers(self, env, keys): + """Remove http headers from environment.""" + for k in keys: + env_key = self._header_to_env_var(k) + try: + del env[env_key] + except KeyError: + pass + + def _get_header(self, env, key, default=None): + """Get http header from environment.""" + env_key = self._header_to_env_var(key) + return env.get(env_key, default) + + def _cache_get(self, token): + """Return token information from cache. + + If token is invalid raise InvalidUserToken + return token only if fresh (not expired). + """ + if self._cache and token: + key = 'tokens/%s' % token + cached = self._cache.get(key) + if cached == 'invalid': + LOG.debug('Cached Token %s is marked unauthorized', token) + raise InvalidUserToken('Token authorization failed') + if cached: + data, expires = cached + if time.time() < float(expires): + LOG.debug('Returning cached token %s', token) + return data + else: + LOG.debug('Cached Token %s seems expired', token) + + def _cache_put(self, token, data): + """Put token data into the cache. + + Stores the parsed expire date in cache allowing + quick check of token freshness on retrieval. + """ + if self._cache and data: + key = 'tokens/%s' % token + if 'token' in data.get('access', {}): + timestamp = data['access']['token']['expires'] + expires = self._iso8601.parse_date(timestamp).strftime('%s') + else: + LOG.error('invalid token format') + return + LOG.debug('Storing %s token in memcache', token) + self._cache.set(key, + (data, expires), + time=self.token_cache_time) + + def _cache_store_invalid(self, token): + """Store invalid token in cache.""" + if self._cache: + key = 'tokens/%s' % token + LOG.debug('Marking token %s as unauthorized in memcache', token) + self._cache.set(key, + 'invalid', + time=self.token_cache_time) + + def cert_file_missing(self, called_proc_err, file_name): + return (called_proc_err.output.find(file_name) + and not os.path.exists(file_name)) + + def verify_uuid_token(self, user_token, retry=True): + """Authenticate user token with keystone. + + :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. + :return token object received from keystone on success + :raise InvalidUserToken if token is rejected + :raise ServiceError if unable to authenticate token + + """ + + headers = {'X-Auth-Token': self.get_admin_token()} + response, data = self._json_request('GET', + '/v2.0/tokens/%s' % user_token, + additional_headers=headers) + + if response.status == 200: + self._cache_put(user_token, data) + return data + if response.status == 404: + # FIXME(ja): I'm assuming the 404 status means that user_token is + # invalid - not that the admin_token is invalid + self._cache_store_invalid(user_token) + LOG.warn("Authorization failed for token %s", user_token) + raise InvalidUserToken('Token authorization failed') + if response.status == 401: + LOG.info('Keystone rejected admin token %s, resetting', headers) + self.admin_token = None + else: + LOG.error('Bad response code while validating token: %s' % + response.status) + if retry: + LOG.info('Retrying validation') + return self._validate_user_token(user_token, False) + else: + LOG.warn("Invalid user token: %s. Keystone response: %s.", + user_token, data) + + raise InvalidUserToken() + + def is_signed_token_revoked(self, signed_text): + """Indicate whether the token appears in the revocation list.""" + revocation_list = self.token_revocation_list + revoked_tokens = revocation_list.get('revoked', []) + if not revoked_tokens: + return + revoked_ids = (x['id'] for x in revoked_tokens) + token_id = utils.hash_signed_token(signed_text) + for revoked_id in revoked_ids: + if token_id == revoked_id: + LOG.debug('Token %s is marked as having been revoked', + token_id) + return True + return False + + def cms_verify(self, data): + """Verifies the signature of the provided data's IAW CMS syntax. + + If either of the certificate files are missing, fetch them and + retry. + """ + while True: + try: + output = cms.cms_verify(data, self.signing_cert_file_name, + self.ca_file_name) + except cms.subprocess.CalledProcessError as err: + if self.cert_file_missing(err, self.signing_cert_file_name): + self.fetch_signing_cert() + continue + if self.cert_file_missing(err, self.ca_file_name): + self.fetch_ca_cert() + continue + raise err + return output + + def verify_signed_token(self, signed_text): + """Check that the token is unrevoked and has a valid signature.""" + if self.is_signed_token_revoked(signed_text): + raise InvalidUserToken('Token has been revoked') + + formatted = cms.token_to_cms(signed_text) + return self.cms_verify(formatted) + + @property + def token_revocation_list_fetched_time(self): + if not self._token_revocation_list_fetched_time: + # If the fetched list has been written to disk, use its + # modification time. + if os.path.exists(self.revoked_file_name): + mtime = os.path.getmtime(self.revoked_file_name) + fetched_time = datetime.datetime.fromtimestamp(mtime) + # Otherwise the list will need to be fetched. + else: + fetched_time = datetime.datetime.min + self._token_revocation_list_fetched_time = fetched_time + return self._token_revocation_list_fetched_time + + @token_revocation_list_fetched_time.setter + def token_revocation_list_fetched_time(self, value): + self._token_revocation_list_fetched_time = value + + @property + def token_revocation_list(self): + timeout = (self.token_revocation_list_fetched_time + + self.token_revocation_list_cache_timeout) + list_is_current = timeutils.utcnow() < timeout + if list_is_current: + # Load the list from disk if required + if not self._token_revocation_list: + with open(self.revoked_file_name, 'r') as f: + self._token_revocation_list = jsonutils.loads(f.read()) + else: + self.token_revocation_list = self.fetch_revocation_list() + return self._token_revocation_list + + @token_revocation_list.setter + def token_revocation_list(self, value): + """Save a revocation list to memory and to disk. + + :param value: A json-encoded revocation list + + """ + self._token_revocation_list = jsonutils.loads(value) + self.token_revocation_list_fetched_time = timeutils.utcnow() + with open(self.revoked_file_name, 'w') as f: + f.write(value) + + def fetch_revocation_list(self, retry=True): + 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 retry: + 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: + raise ServiceError('Unable to fetch token revocation list.') + if (not 'signed' in data): + raise ServiceError('Revocation list inmproperly formatted.') + return self.cms_verify(data['signed']) + + def fetch_signing_cert(self): + response, data = self._http_request('GET', + '/v2.0/certificates/signing') + try: + #todo check response + certfile = open(self.signing_cert_file_name, 'w') + certfile.write(data) + certfile.close() + except (AssertionError, KeyError): + LOG.warn("Unexpected response from keystone service: %s", data) + raise ServiceError('invalid json response') + + def fetch_ca_cert(self): + response, data = self._http_request('GET', + '/v2.0/certificates/ca') + try: + #todo check response + certfile = open(self.ca_file_name, 'w') + certfile.write(data) + certfile.close() + except (AssertionError, KeyError): + LOG.warn("Unexpected response from keystone service: %s", data) + raise ServiceError('invalid json response') + + +def filter_factory(global_conf, **local_conf): + """Returns a WSGI filter app for use with paste.deploy.""" + conf = global_conf.copy() + conf.update(local_conf) + + def auth_filter(app): + return AuthProtocol(app, conf) + return auth_filter + + +def app_factory(global_conf, **local_conf): + conf = global_conf.copy() + conf.update(local_conf) + return AuthProtocol(None, conf) diff --git a/keystoneclient/middleware/test.py b/keystoneclient/middleware/test.py new file mode 100644 index 0000000..e5c1171 --- /dev/null +++ b/keystoneclient/middleware/test.py @@ -0,0 +1,67 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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. + +# +# Test support for middleware authentication +# + +import os +import sys + + +ROOTDIR = os.path.dirname(os.path.abspath(os.curdir)) + + +def rootdir(*p): + return os.path.join(ROOTDIR, *p) + + +class NoModule(object): + """A mixin class to provide support for unloading/disabling modules.""" + + def __init__(self, *args, **kw): + super(NoModule, self).__init__(*args, **kw) + self._finders = [] + self._cleared_modules = {} + + def tearDown(self): + super(NoModule, self).tearDown() + for finder in self._finders: + sys.meta_path.remove(finder) + sys.modules.update(self._cleared_modules) + + def clear_module(self, module): + cleared_modules = {} + for fullname in sys.modules.keys(): + if fullname == module or fullname.startswith(module + '.'): + cleared_modules[fullname] = sys.modules.pop(fullname) + return cleared_modules + + def disable_module(self, module): + """Ensure ImportError for the specified module.""" + + # Clear 'module' references in sys.modules + self._cleared_modules.update(self.clear_module(module)) + + # Disallow further imports of 'module' + class NoModule(object): + def find_module(self, fullname, path): + if fullname == module or fullname.startswith(module + '.'): + raise ImportError + + finder = NoModule() + self._finders.append(finder) + sys.meta_path.insert(0, finder) |