diff --git a/doc/source/images/graphs_authComp.svg b/doc/source/images/graphs_authComp.svg new file mode 100644 index 000000000..6be629c12 --- /dev/null +++ b/doc/source/images/graphs_authComp.svg @@ -0,0 +1,48 @@ + + + + + + +AuthComp + + +AuthComp + +Auth +Component + + + +AuthComp->Reject + + +Reject +Unauthenticated +Requests + + +Service + +OpenStack +Service + + +AuthComp->Service + + +Forward +Authenticated +Requests + + + +Start->AuthComp + + + + + diff --git a/doc/source/images/graphs_authCompDelegate.svg b/doc/source/images/graphs_authCompDelegate.svg new file mode 100644 index 000000000..4788829a4 --- /dev/null +++ b/doc/source/images/graphs_authCompDelegate.svg @@ -0,0 +1,53 @@ + + + + + + +AuthCompDelegate + + +AuthComp + +Auth +Component + + + +AuthComp->Reject + + +Reject Requests +Indicated by the Service + + +Service + +OpenStack +Service + + +AuthComp->Service + + +Forward Requests +with Identiy Status + + +Service->AuthComp + + +Send Response OR +Reject Message + + + +Start->AuthComp + + + + + diff --git a/doc/source/middlewarearchitecture.rst b/doc/source/middlewarearchitecture.rst new file mode 100644 index 000000000..59a6db025 --- /dev/null +++ b/doc/source/middlewarearchitecture.rst @@ -0,0 +1,309 @@ +.. + Copyright 2011-2012 OpenStack, LLC + All Rights Reserved. + + 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. + +======================= +Middleware Architecture +======================= + +Abstract +======== + +The Keystone middleware architecture supports a common authentication protocol +in use between the OpenStack projects. By using keystone as a common +authentication and authorization mechanisms, the OpenStack project can plug in +to existing authentication and authorization systems in use by existing +environments. + +In this document, we describe the architecture and responsibilities of the +authentication middleware which acts as the internal API mechanism for +OpenStack projects based on the WSGI standard. + +For the architecture of keystone and its services, please see +:doc:`architecture`. This documentation primarily describes the implementation +in ``keystoneclient/middleware/auth_token.py`` +(:py:class:`keystoneclient.middleware.auth_token.AuthProtocol`) + +Specification Overview +====================== + +'Authentication' is the process of determining that users are who they say they +are. Typically, 'authentication protocols' such as HTTP Basic Auth, Digest +Access, public key, token, etc, are used to verify a user's identity. In this +document, we define an ''authentication component'' as a software module that +implements an authentication protocol for an OpenStack service. OpenStack is +using a token based mechanism to represent authentication and authorization. + +At a high level, an authentication middleware component is a proxy that +intercepts HTTP calls from clients and populates HTTP headers in the request +context for other WSGI middleware or applications to use. The general flow +of the middleware processing is: + +* clear any existing authorization headers to prevent forgery +* collect the token from the existing HTTP request headers +* validate the token + + * if valid, populate additional headers representing the identity that has + been authenticated and authorized + * in invalid, or not token present, reject the request (HTTPUnauthorized) + or pass along a header indicating the request is unauthorized (configurable + in the middleware) + * if the keystone service is unavailable to validate the token, reject + the request with HTTPServiceUnavailable. + +.. _authComponent: + +Authentication Component +------------------------ + +Figure 1. Authentication Component + +.. image:: images/graphs_authComp.svg + :width: 100% + :height: 180 + :alt: An Authentication Component + +The middleware may also be configured to operated in a 'delegated mode'. +In this mode, the decision reject an unauthenticated client is delegated to +the OpenStack service, as illustrated in :ref:`authComponentDelegated`. + +Here, requests are forwarded to the OpenStack service with an identity status +message that indicates whether the client's identity has been confirmed or is +indeterminate. It is the OpenStack service that decides whether or not a reject +message should be sent to the client. + +.. _authComponentDelegated: + +Authentication Component (Delegated Mode) +----------------------------------------- + +Figure 2. Authentication Component (Delegated Mode) + +.. image:: images/graphs_authCompDelegate.svg + :width: 100% + :height: 180 + :alt: An Authentication Component (Delegated Mode) + +.. _deployStrategies: + +Deployment Strategy +=================== + +The middleware is intended to be used inline with OpenStack wsgi components, +based on the openstack-common WSGI middleware class. It is typically deployed +as a configuration element in a paste configuration pipeline of other +middleware components, with the pipeline terminating in the service +application. The middleware conforms to the python WSGI standard [PEP-333]_. +In initializing the middleware, a configuration item (which acts like a python +dictionary) is passed to the middleware with relevant configuration options. + +Configuration +------------- + +The middleware is configured within the config file of the main application as +a WSGI component. Example for the auth_token middleware:: + + [app:myService] + paste.app_factory = myService:app_factory + + [pipeline:main] + pipeline = tokenauth myService + + [filter:tokenauth] + paste.filter_factory = keystone.middleware.auth_token:filter_factory + auth_host = 127.0.0.1 + auth_port = 35357 + auth_protocol = http + auth_uri = http://127.0.0.1:5000/ + admin_token = Super999Sekret888Password777 + admin_user = admin + admin_password = SuperSekretPassword + admin_tenant_name = service + ;Uncomment next line to use Swift MemcacheRing + ;cache = swift.cache + ;Uncomment next line and check ip:port to use memcached to cache tokens + ;memcache_servers = 127.0.0.1:11211 + ;Uncomment next 2 lines to turn on memcache protection + ;memcache_security_strategy = ENCRYPT + ;memcache_secret_key = change_me + ;Uncomment next 2 lines if Keystone server is validating client cert + ;certfile = + ;keyfile = + +For services which have separate paste-deploy ini file, auth_token middleware +can be alternatively configured in [keystone_authtoken] section in the main +config file. For example in Nova, all middleware parameters can be removed +from api-paste.ini:: + + [filter:authtoken] + paste.filter_factory = keystone.middleware.auth_token:filter_factory + +and set in nova.conf:: + + [DEFAULT] + ... + auth_strategy=keystone + + [keystone_authtoken] + auth_host = 127.0.0.1 + auth_port = 35357 + auth_protocol = http + auth_uri = http://127.0.0.1:5000/ + admin_user = admin + admin_password = SuperSekretPassword + admin_tenant_name = service + +Note that middleware parameters in paste config take priority, they must be +removed to use values in [keystone_authtoken] section. + +Configuration Options +--------------------- + +* ``auth_host``: (required) the host providing the keystone service API endpoint + for validating and requesting tokens +* ``admin_token``: either this or the following three options are required. If + set, this is a single shared secret with the keystone configuration used to + validate tokens. +* ``admin_user``, ``admin_password``, ``admin_tenant_name``: if ``admin_token`` + is not set, or invalid, then admin_user, admin_password, and + admin_tenant_name are defined as a service account which is expected to have + been previously configured in Keystone to validate user tokens. + +* ``delay_auth_decision``: (optional, default `0`) (off). If on, the middleware + will not reject invalid auth requests, but will delegate that decision to + downstream WSGI components. +* ``auth_port``: (optional, default `35357`) the port used to validate tokens +* ``auth_protocol``: (optional, default `https`) +* ``auth_uri``: (optional, defaults to `auth_protocol`://`auth_host`:`auth_port`) +* ``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. + +Caching for improved response +----------------------------- + +In order to prevent every service request, the middleware may be configured +to utilize a cache, and the keystone API returns the tokens with an +expiration (configurable in duration on the keystone service). The middleware +supports memcache based caching. + +* ``memcache_servers``: (optonal) if defined, the memcache server(s) to use for + cacheing. It will be ignored if Swift MemcacheRing is used instead. +* ``token_cache_time``: (optional, default 300 seconds) Only valid if + memcache_servers is defined. + +When deploying auth_token middleware with Swift, user may elect +to use Swift MemcacheRing instead of the local Keystone memcache. +The Swift MemcacheRing object is passed in from the request environment +and it defaults to 'swift.cache'. However it could be +different, depending on deployment. To use Swift MemcacheRing, you must +provide the ``cache`` option. + +* ``cache``: (optional) if defined, the environment key where the Swift + MemcacheRing object is stored. + +Memcached and System Time +========================= + +When using `memcached`_ with ``auth_token`` middleware, ensure that the system +time of memcached hosts is set to UTC. Memcached uses the host's system +time in determining whether a key has expired, whereas Keystone sets +key expiry in UTC. The timezone used by Keystone and memcached must +match if key expiry is to behave as expected. + +.. _`memcached`: http://memcached.org/ + +Memcache Protection +=================== + +When using memcached, we are storing user tokens and token validation +information into the cache as raw data. Which means anyone who have access +to the memcache servers can read and modify data stored there. To mitigate +this risk, ``auth_token`` middleware provides an option to either encrypt +or authenticate the token data stored in the cache. + +* ``memcache_security_strategy``: (optional) if defined, indicate whether token + data should be encrypted or authenticated. Acceptable values are ``ENCRYPT`` + or ``MAC``. If ``ENCRYPT``, token data is encrypted in the cache. If + ``MAC``, token data is authenticated (with HMAC) in the cache. If its value + is neither ``MAC`` nor ``ENCRYPT``, ``auth_token`` will raise an exception + on initialization. +* ``memcache_secret_key``: (optional, mandatory if + ``memcache_security_strategy`` is defined) if defined, + a random string to be used for key derivation. If + ``memcache_security_strategy`` is defined and ``memcache_secret_key`` is + absent, ``auth_token`` will raise an exception on initialization. + +Exchanging User Information +=========================== + +The middleware expects to find a token representing the user with the header +``X-Auth-Token`` or ``X-Storage-Token``. `X-Storage-Token` is supported for +swift/cloud files and for legacy Rackspace use. If the token isn't present and +the middleware is configured to not delegate auth responsibility, it will +respond to the HTTP request with HTTPUnauthorized, returning the header +``WWW-Authenticate`` with the value `Keystone uri='...'` to indicate where to +request a token. The auth_uri returned is configured with the middleware. + +The authentication middleware extends the HTTP request with the header +``X-Identity-Status``. If a request is successfully authenticated, the value +is set to `Confirmed`. If the middleware is delegating the auth decision to the +service, then the status is set to `Invalid` if the auth request was +unsuccessful. + +Extended the request with additional User Information +----------------------------------------------------- + +:py:class:`keystone.middleware.auth_token.AuthProtocol` extends the request +with additional information if the user has been authenticated. + + +X-Identity-Status + Provides information on whether the request was authenticated or not. + +X-Tenant-Id + The unique, immutable tenant Id + +X-Tenant-Name + The unique, but mutable (it can change) tenant name. + +X-User-Id + The user id of the user used to log in + +X-User-Name + The username used to log in + +X-Roles + The roles associated with that user + +Deprecated additions +-------------------- + +X-Tenant + Provides the tenant name. This is to support any legacy implementations + before Keystone switched to an ID/Name schema for tenants. + +X-User + The username used to log in. This is to support any legacy implementations + before Keystone switched to an ID/Name schema for tenants. + +X-Role + The roles associated with that user + +References +========== + +.. [PEP-333] pep0333 Phillip J Eby. 'Python Web Server Gateway Interface + v1.0.'' http://www.python.org/dev/peps/pep-0333/. diff --git a/keystoneclient/middleware/auth_token.py b/keystoneclient/middleware/auth_token.py index 6f9d7a87a..7d332205e 100644 --- a/keystoneclient/middleware/auth_token.py +++ b/keystoneclient/middleware/auth_token.py @@ -115,6 +115,7 @@ import webob.exc from keystoneclient.openstack.common import jsonutils from keystoneclient.common import cms from keystoneclient import utils +from keystoneclient.middleware import memcache_crypt from keystoneclient.openstack.common import timeutils CONF = None @@ -171,6 +172,8 @@ opts = [ default=os.path.expanduser('~/keystone-signing')), cfg.ListOpt('memcache_servers'), cfg.IntOpt('token_cache_time', default=300), + cfg.StrOpt('memcache_security_strategy', default=None), + cfg.StrOpt('memcache_secret_key', default=None), ] CONF.register_opts(opts, group='keystone_authtoken') @@ -267,7 +270,17 @@ class AuthProtocol(object): # Token caching via memcache self._cache = None + self._use_keystone_cache = False self._cache_initialized = False # cache already initialzied? + # memcache value treatment, ENCRYPT or MAC + self._memcache_security_strategy = \ + self._conf_get('memcache_security_strategy') + if self._memcache_security_strategy is not None: + self._memcache_security_strategy = \ + self._memcache_security_strategy.upper() + self._memcache_secret_key = \ + self._conf_get('memcache_secret_key') + self._assert_valid_memcache_protection_config() # 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 @@ -275,6 +288,15 @@ class AuthProtocol(object): cache_timeout = datetime.timedelta(seconds=0) self.token_revocation_list_cache_timeout = cache_timeout + def _assert_valid_memcache_protection_config(self): + if self._memcache_security_strategy: + if self._memcache_security_strategy not in ('MAC', 'ENCRYPT'): + raise Exception('memcache_security_strategy must be ' + 'ENCRYPT or MAC') + if not self._memcache_secret_key: + raise Exception('mecmache_secret_key must be defined when ' + 'a memcache_security_strategy is defined') + def _init_cache(self, env): cache = self._conf_get('cache') memcache_servers = self._conf_get('memcache_servers') @@ -290,6 +312,7 @@ class AuthProtocol(object): import memcache self.LOG.info('Using Keystone memcache for caching token') self._cache = memcache.Client(memcache_servers) + self._use_keystone_cache = True except ImportError as e: msg = 'disabled caching due to missing libraries %s' % (e) self.LOG.warn(msg) @@ -659,6 +682,54 @@ class AuthProtocol(object): env_key = self._header_to_env_var(key) return env.get(env_key, default) + def _protect_cache_value(self, token, data): + """ Encrypt or sign data if necessary. """ + try: + if self._memcache_security_strategy == 'ENCRYPT': + return memcache_crypt.encrypt_data(token, + self._memcache_secret_key, + data) + elif self._memcache_security_strategy == 'MAC': + return memcache_crypt.sign_data(token, data) + else: + return data + except: + msg = 'Failed to encrypt/sign cache data.' + self.LOG.exception(msg) + return data + + def _unprotect_cache_value(self, token, data): + """ Decrypt or verify signed data if necessary. """ + if data is None: + return data + + try: + if self._memcache_security_strategy == 'ENCRYPT': + return memcache_crypt.decrypt_data(token, + self._memcache_secret_key, + data) + elif self._memcache_security_strategy == 'MAC': + return memcache_crypt.verify_signed_data(token, data) + else: + return data + except: + msg = 'Failed to decrypt/verify cache data.' + self.LOG.exception(msg) + # this should have the same effect as data not found in cache + return None + + def _get_cache_key(self, token): + """ Return the cache key. + + Do not use clear token as key if memcache protection is on. + + """ + htoken = token + if self._memcache_security_strategy in ('ENCRYPT', 'MAC'): + derv_token = token + self._memcache_secret_key + htoken = memcache_crypt.hash_data(derv_token) + return 'tokens/%s' % htoken + def _cache_get(self, token): """Return token information from cache. @@ -666,8 +737,9 @@ class AuthProtocol(object): return token only if fresh (not expired). """ if self._cache and token: - key = 'tokens/%s' % token + key = self._get_cache_key(token) cached = self._cache.get(key) + cached = self._unprotect_cache_value(token, cached) if cached == 'invalid': self.LOG.debug('Cached Token %s is marked unauthorized', token) raise InvalidUserToken('Token authorization failed') @@ -679,14 +751,32 @@ class AuthProtocol(object): else: self.LOG.debug('Cached Token %s seems expired', token) + def _cache_store(self, token, data, expires=None): + """ Store value into memcache. """ + key = self._get_cache_key(token) + data = self._protect_cache_value(token, data) + data_to_store = data + if expires: + data_to_store = (data, expires) + # we need to special-case set() because of the incompatibility between + # Swift MemcacheRing and python-memcached. See + # https://bugs.launchpad.net/swift/+bug/1095730 + if self._use_keystone_cache: + self._cache.set(key, + data_to_store, + time=self.token_cache_time) + else: + self._cache.set(key, + data_to_store, + timeout=self.token_cache_time) + def _cache_put(self, token, data): - """Put token data into the cache. + """ 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 = timeutils.parse_isotime(timestamp).strftime('%s') @@ -694,19 +784,14 @@ class AuthProtocol(object): self.LOG.error('invalid token format') return self.LOG.debug('Storing %s token in memcache', token) - self._cache.set(key, - (data, expires), - time=self.token_cache_time) + self._cache_store(token, data, expires) def _cache_store_invalid(self, token): """Store invalid token in cache.""" if self._cache: - key = 'tokens/%s' % token self.LOG.debug( 'Marking token %s as unauthorized in memcache', token) - self._cache.set(key, - 'invalid', - time=self.token_cache_time) + self._cache_store(token, 'invalid') def cert_file_missing(self, called_proc_err, file_name): return (called_proc_err.output.find(file_name) diff --git a/keystoneclient/middleware/memcache_crypt.py b/keystoneclient/middleware/memcache_crypt.py new file mode 100755 index 000000000..91e261da0 --- /dev/null +++ b/keystoneclient/middleware/memcache_crypt.py @@ -0,0 +1,157 @@ +# 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. + +""" +Utilities for memcache encryption and integrity check. + +Data is serialized before been encrypted or MACed. Encryption have a +dependency on the pycrypto. If pycrypto is not available, +CryptoUnabailableError will be raised. + +Encrypted data stored in memcache are prefixed with '{ENCRYPT:AES256}'. + +MACed data stored in memcache are prefixed with '{MAC:SHA1}'. + +""" + +import base64 +import functools +import hashlib +import json +import os + +# make sure pycrypt is available +try: + from Crypto.Cipher import AES +except ImportError: + AES = None + + +# prefix marker indicating data is HMACed (signed by a secret key) +MAC_MARKER = '{MAC:SHA1}' +# prefix marker indicating data is encrypted +ENCRYPT_MARKER = '{ENCRYPT:AES256}' + + +class InvalidMacError(Exception): + """ raise when unable to verify MACed data + + This usually indicates that data had been expectedly modified in memcache. + + """ + pass + + +class DecryptError(Exception): + """ raise when unable to decrypt encrypted data + + """ + pass + + +class CryptoUnavailableError(Exception): + """ raise when Python Crypto module is not available + + """ + pass + + +def assert_crypto_availability(f): + """ Ensure Crypto module is available. """ + + @functools.wraps(f) + def wrapper(*args, **kwds): + if AES is None: + raise CryptoUnavailableError() + return f(*args, **kwds) + return wrapper + + +def generate_aes_key(token, secret): + """ Generates and returns a 256 bit AES key, based on sha256 hash. """ + return hashlib.sha256(token + secret).digest() + + +def compute_mac(token, serialized_data): + """ Computes and returns the base64 encoded MAC. """ + return hash_data(serialized_data + token) + + +def hash_data(data): + """ Return the base64 encoded SHA1 hash of the data. """ + return base64.b64encode(hashlib.sha1(data).digest()) + + +def sign_data(token, data): + """ MAC the data using SHA1. """ + mac_data = {} + mac_data['serialized_data'] = json.dumps(data) + mac = compute_mac(token, mac_data['serialized_data']) + mac_data['mac'] = mac + md = MAC_MARKER + base64.b64encode(json.dumps(mac_data)) + return md + + +def verify_signed_data(token, data): + """ Verify data integrity by ensuring MAC is valid. """ + if data.startswith(MAC_MARKER): + try: + data = data[len(MAC_MARKER):] + mac_data = json.loads(base64.b64decode(data)) + mac = compute_mac(token, mac_data['serialized_data']) + if mac != mac_data['mac']: + raise InvalidMacError('invalid MAC; expect=%s, actual=%s' % + (mac_data['mac'], mac)) + return json.loads(mac_data['serialized_data']) + except: + raise InvalidMacError('invalid MAC; data appeared to be corrupted') + else: + # doesn't appear to be MACed data + return data + + +@assert_crypto_availability +def encrypt_data(token, secret, data): + """ Encryptes the data with the given secret key. """ + iv = os.urandom(16) + aes_key = generate_aes_key(token, secret) + cipher = AES.new(aes_key, AES.MODE_CFB, iv) + data = json.dumps(data) + encoded_data = base64.b64encode(iv + cipher.encrypt(data)) + encoded_data = ENCRYPT_MARKER + encoded_data + return encoded_data + + +@assert_crypto_availability +def decrypt_data(token, secret, data): + """ Decrypt the data with the given secret key. """ + if data.startswith(ENCRYPT_MARKER): + try: + # encrypted data + encoded_data = data[len(ENCRYPT_MARKER):] + aes_key = generate_aes_key(token, secret) + decoded_data = base64.b64decode(encoded_data) + iv = decoded_data[:16] + encrypted_data = decoded_data[16:] + cipher = AES.new(aes_key, AES.MODE_CFB, iv) + decrypted_data = cipher.decrypt(encrypted_data) + return json.loads(decrypted_data) + except: + raise DecryptError('data appeared to be corrupted') + else: + # doesn't appear to be encrypted data + return data diff --git a/tests/test_auth_token_middleware.py b/tests/test_auth_token_middleware.py index 324bf0118..40fc4018f 100644 --- a/tests/test_auth_token_middleware.py +++ b/tests/test_auth_token_middleware.py @@ -26,6 +26,7 @@ import webob from keystoneclient.common import cms from keystoneclient import utils from keystoneclient.middleware import auth_token +from keystoneclient.middleware import memcache_crypt from keystoneclient.openstack.common import jsonutils from keystoneclient.openstack.common import timeutils from keystoneclient.middleware import test @@ -235,6 +236,29 @@ class FakeMemcache(object): self.set_key = key +class FakeSwiftMemcacheRing(object): + def __init__(self): + self.set_key = None + self.set_value = None + self.token_expiration = None + + def get(self, key): + data = TOKEN_RESPONSES[SIGNED_TOKEN_SCOPED_KEY].copy() + if not data or key != "tokens/%s" % (data['access']['token']['id']): + return + if not self.token_expiration: + dt = datetime.datetime.now() + datetime.timedelta(minutes=5) + self.token_expiration = dt.strftime("%s") + dt = datetime.datetime.now() + datetime.timedelta(hours=24) + ks_expires = dt.isoformat() + data['access']['token']['expires'] = ks_expires + return (data, str(self.token_expiration)) + + def set(self, key, value, serialize=True, timeout=0): + self.set_value = value + self.set_key = key + + class FakeHTTPResponse(object): def __init__(self, status, body): self.status = status @@ -641,6 +665,7 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): req = webob.Request.blank('/') req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED self.middleware._cache = FakeMemcache() + self.middleware._use_keystone_cache = True self.middleware(req.environ, self.start_fake_response) self.assertEqual(self.middleware._cache.set_value, None) @@ -648,6 +673,7 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): req = webob.Request.blank('/') req.headers['X-Auth-Token'] = 'invalid-token' self.middleware._cache = FakeMemcache() + self.middleware._use_keystone_cache = True self.middleware(req.environ, self.start_fake_response) self.assertEqual(self.middleware._cache.set_value, "invalid") @@ -655,6 +681,17 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): req = webob.Request.blank('/') req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED self.middleware._cache = FakeMemcache() + self.middleware._use_keystone_cache = True + expired = datetime.datetime.now() - datetime.timedelta(minutes=1) + self.middleware._cache.token_expiration = float(expired.strftime("%s")) + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(len(self.middleware._cache.set_value), 2) + + def test_swift_memcache_set_expired(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED + self.middleware._cache = FakeSwiftMemcacheRing() + self.middleware._use_keystone_cache = False expired = datetime.datetime.now() - datetime.timedelta(minutes=1) self.middleware._cache.token_expiration = float(expired.strftime("%s")) self.middleware(req.environ, self.start_fake_response) @@ -715,6 +752,155 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): seconds=40) self.assertFalse(auth_token.will_expire_soon(fortyseconds)) + def test_encrypt_cache_data(self): + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_security_strategy': 'encrypt', + 'memcache_secret_key': 'mysecret', + } + auth = auth_token.AuthProtocol(FakeApp(), conf) + encrypted_data = \ + auth._protect_cache_value('token', + TOKEN_RESPONSES[UUID_TOKEN_DEFAULT]) + self.assertEqual('{ENCRYPT:AES256}', encrypted_data[:16]) + self.assertDictEqual( + TOKEN_RESPONSES[UUID_TOKEN_DEFAULT], + auth._unprotect_cache_value('token', encrypted_data)) + # should return None if unable to decrypt + self.assertIsNone( + auth._unprotect_cache_value('token', '{ENCRYPT:AES256}corrupted')) + self.assertIsNone( + auth._unprotect_cache_value('mykey', encrypted_data)) + + def test_sign_cache_data(self): + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_security_strategy': 'mac', + 'memcache_secret_key': 'mysecret', + } + auth = auth_token.AuthProtocol(FakeApp(), conf) + signed_data = \ + auth._protect_cache_value('mykey', + TOKEN_RESPONSES[UUID_TOKEN_DEFAULT]) + expected = '{MAC:SHA1}' + self.assertEqual( + signed_data[:10], + expected) + self.assertDictEqual( + TOKEN_RESPONSES[UUID_TOKEN_DEFAULT], + auth._unprotect_cache_value('mykey', signed_data)) + # should return None on corrupted data + self.assertIsNone( + auth._unprotect_cache_value('mykey', '{MAC:SHA1}corrupted')) + + def test_no_memcache_protection(self): + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_secret_key': 'mysecret', + } + auth = auth_token.AuthProtocol(FakeApp(), conf) + data = auth._protect_cache_value('mykey', 'This is a test!') + self.assertEqual(data, 'This is a test!') + self.assertEqual( + 'This is a test!', + auth._unprotect_cache_value('mykey', data)) + + def test_get_cache_key(self): + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_secret_key': 'mysecret', + } + auth = auth_token.AuthProtocol(FakeApp(), conf) + self.assertEqual( + 'tokens/mytoken', + auth._get_cache_key('mytoken')) + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_security_strategy': 'mac', + 'memcache_secret_key': 'mysecret', + } + auth = auth_token.AuthProtocol(FakeApp(), conf) + expected = 'tokens/' + memcache_crypt.hash_data('mytoken' + 'mysecret') + self.assertEqual(auth._get_cache_key('mytoken'), expected) + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_security_strategy': 'Encrypt', + 'memcache_secret_key': 'abc!', + } + auth = auth_token.AuthProtocol(FakeApp(), conf) + expected = 'tokens/' + memcache_crypt.hash_data('mytoken' + 'abc!') + self.assertEqual(auth._get_cache_key('mytoken'), expected) + + def test_assert_valid_memcache_protection_config(self): + # test missing memcache_secret_key + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_security_strategy': 'Encrypt', + } + self.assertRaises(Exception, auth_token.AuthProtocol, + FakeApp(), conf) + # test invalue memcache_security_strategy + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_security_strategy': 'whatever', + } + self.assertRaises(Exception, auth_token.AuthProtocol, + FakeApp(), conf) + # test missing memcache_secret_key + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_security_strategy': 'mac', + } + self.assertRaises(Exception, auth_token.AuthProtocol, + FakeApp(), conf) + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_security_strategy': 'Encrypt', + 'memcache_secret_key': '' + } + self.assertRaises(Exception, auth_token.AuthProtocol, + FakeApp(), conf) + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + 'memcache_security_strategy': 'mAc', + 'memcache_secret_key': '' + } + self.assertRaises(Exception, auth_token.AuthProtocol, + FakeApp(), conf) + class TokenEncodingTest(testtools.TestCase): def test_unquoted_token(self): diff --git a/tests/test_memcache_crypt.py b/tests/test_memcache_crypt.py new file mode 100644 index 000000000..b2281d935 --- /dev/null +++ b/tests/test_memcache_crypt.py @@ -0,0 +1,56 @@ +import testtools + +from keystoneclient.middleware import memcache_crypt + + +class MemcacheCryptPositiveTests(testtools.TestCase): + def test_generate_aes_key(self): + self.assertEqual( + len(memcache_crypt.generate_aes_key('Gimme Da Key', 'hush')), 32) + + def test_compute_mac(self): + self.assertEqual( + memcache_crypt.compute_mac('mykey', 'This is a test!'), + 'tREu41yR5tEgeBWIuv9ag4AeKA8=') + + def test_sign_data(self): + expected = '{MAC:SHA1}eyJtYWMiOiAiM0FrQmdPZHRybGo1RFFESHA1eUxqcDVq' +\ + 'Si9BPSIsICJzZXJpYWxpemVkX2RhdGEiOiAiXCJUaGlzIGlzIGEgdG' +\ + 'VzdCFcIiJ9' + self.assertEqual( + memcache_crypt.sign_data('mykey', 'This is a test!'), + expected) + + def test_verify_signed_data(self): + signed = memcache_crypt.sign_data('mykey', 'Testz') + self.assertEqual( + memcache_crypt.verify_signed_data('mykey', signed), + 'Testz') + self.assertEqual( + memcache_crypt.verify_signed_data('aasSFWE13WER', 'not MACed'), + 'not MACed') + + def test_encrypt_data(self): + expected = '{ENCRYPT:AES256}' + self.assertEqual( + memcache_crypt.encrypt_data('mykey', 'mysecret', + 'This is a test!')[:16], + expected) + + def test_decrypt_data(self): + encrypted = memcache_crypt.encrypt_data('mykey', 'mysecret', 'Testz') + self.assertEqual( + memcache_crypt.decrypt_data('mykey', 'mysecret', encrypted), + 'Testz') + self.assertEqual( + memcache_crypt.decrypt_data('mykey', 'mysecret', + 'Not Encrypted!'), + 'Not Encrypted!') + + def test_no_pycrypt(self): + aes = memcache_crypt.AES + memcache_crypt.AES = None + self.assertRaises(memcache_crypt.CryptoUnavailableError, + memcache_crypt.encrypt_data, 'token', 'secret', + 'data') + memcache_crypt.AES = aes diff --git a/tools/test-requires b/tools/test-requires index 402c4092a..d0abb3298 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -10,6 +10,7 @@ nose-exclude openstack.nose_plugin nosehtmloutput pep8==1.3.3 +pycrypto sphinx>=1.1.2 testtools>=0.9.22 WebOb>=1.0.8