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 @@
+
+
+
+
+
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 @@
+
+
+
+
+
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