Blueprint memcache-protection: enable memcache value encryption/integrity check
DocImpact Change-Id: I8b733256a3c2cdcf7c2ec5edac491ac4739aa847
This commit is contained in:
48
doc/source/images/graphs_authComp.svg
Normal file
48
doc/source/images/graphs_authComp.svg
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.27.20101213.0545 (20101213.0545)
|
||||
-->
|
||||
<!-- Title: AuthComp Pages: 1 -->
|
||||
<svg width="510pt" height="118pt"
|
||||
viewBox="0.00 0.00 510.00 118.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph1" class="graph" transform="scale(1 1) rotate(0) translate(4 114)">
|
||||
<title>AuthComp</title>
|
||||
<polygon fill="white" stroke="white" points="-4,5 -4,-114 507,-114 507,5 -4,5"/>
|
||||
<!-- AuthComp -->
|
||||
<g id="node2" class="node"><title>AuthComp</title>
|
||||
<polygon fill="#fdefe3" stroke="#c00000" points="292,-65 194,-65 194,-25 292,-25 292,-65"/>
|
||||
<text text-anchor="middle" x="243" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">Auth</text>
|
||||
<text text-anchor="middle" x="243" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Component</text>
|
||||
</g>
|
||||
<!-- Reject -->
|
||||
<!-- AuthComp->Reject -->
|
||||
<g id="edge3" class="edge"><title>AuthComp->Reject</title>
|
||||
<path fill="none" stroke="black" d="M193.933,-51.2787C157.514,-55.939 108.38,-62.2263 73.8172,-66.649"/>
|
||||
<polygon fill="black" stroke="black" points="73.0637,-63.2168 63.5888,-67.9578 73.9522,-70.1602 73.0637,-63.2168"/>
|
||||
<text text-anchor="middle" x="129" y="-97.4" font-family="Times,serif" font-size="14.00">Reject</text>
|
||||
<text text-anchor="middle" x="129" y="-82.4" font-family="Times,serif" font-size="14.00">Unauthenticated</text>
|
||||
<text text-anchor="middle" x="129" y="-67.4" font-family="Times,serif" font-size="14.00">Requests</text>
|
||||
</g>
|
||||
<!-- Service -->
|
||||
<g id="node6" class="node"><title>Service</title>
|
||||
<polygon fill="#d1ebf1" stroke="#1f477d" points="502,-65 408,-65 408,-25 502,-25 502,-65"/>
|
||||
<text text-anchor="middle" x="455" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">OpenStack</text>
|
||||
<text text-anchor="middle" x="455" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Service</text>
|
||||
</g>
|
||||
<!-- AuthComp->Service -->
|
||||
<g id="edge5" class="edge"><title>AuthComp->Service</title>
|
||||
<path fill="none" stroke="black" d="M292.17,-45C323.626,-45 364.563,-45 397.52,-45"/>
|
||||
<polygon fill="black" stroke="black" points="397.917,-48.5001 407.917,-45 397.917,-41.5001 397.917,-48.5001"/>
|
||||
<text text-anchor="middle" x="350" y="-77.4" font-family="Times,serif" font-size="14.00">Forward</text>
|
||||
<text text-anchor="middle" x="350" y="-62.4" font-family="Times,serif" font-size="14.00">Authenticated</text>
|
||||
<text text-anchor="middle" x="350" y="-47.4" font-family="Times,serif" font-size="14.00">Requests</text>
|
||||
</g>
|
||||
<!-- Start -->
|
||||
<!-- Start->AuthComp -->
|
||||
<g id="edge7" class="edge"><title>Start->AuthComp</title>
|
||||
<path fill="none" stroke="black" d="M59.1526,-21.4745C90.4482,-25.4792 142.816,-32.1802 183.673,-37.4084"/>
|
||||
<polygon fill="black" stroke="black" points="183.43,-40.9057 193.793,-38.7034 184.318,-33.9623 183.43,-40.9057"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
53
doc/source/images/graphs_authCompDelegate.svg
Normal file
53
doc/source/images/graphs_authCompDelegate.svg
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.27.20101213.0545 (20101213.0545)
|
||||
-->
|
||||
<!-- Title: AuthCompDelegate Pages: 1 -->
|
||||
<svg width="588pt" height="104pt"
|
||||
viewBox="0.00 0.00 588.00 104.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph1" class="graph" transform="scale(1 1) rotate(0) translate(4 100)">
|
||||
<title>AuthCompDelegate</title>
|
||||
<polygon fill="white" stroke="white" points="-4,5 -4,-100 585,-100 585,5 -4,5"/>
|
||||
<!-- AuthComp -->
|
||||
<g id="node2" class="node"><title>AuthComp</title>
|
||||
<polygon fill="#fdefe3" stroke="#c00000" points="338,-65 240,-65 240,-25 338,-25 338,-65"/>
|
||||
<text text-anchor="middle" x="289" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">Auth</text>
|
||||
<text text-anchor="middle" x="289" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Component</text>
|
||||
</g>
|
||||
<!-- Reject -->
|
||||
<!-- AuthComp->Reject -->
|
||||
<g id="edge3" class="edge"><title>AuthComp->Reject</title>
|
||||
<path fill="none" stroke="black" d="M239.6,-50.1899C191.406,-55.2531 118.917,-62.8686 73.5875,-67.6309"/>
|
||||
<polygon fill="black" stroke="black" points="73.0928,-64.1635 63.5132,-68.6893 73.8242,-71.1252 73.0928,-64.1635"/>
|
||||
<text text-anchor="middle" x="152" y="-83.4" font-family="Times,serif" font-size="14.00">Reject Requests</text>
|
||||
<text text-anchor="middle" x="152" y="-68.4" font-family="Times,serif" font-size="14.00">Indicated by the Service</text>
|
||||
</g>
|
||||
<!-- Service -->
|
||||
<g id="node6" class="node"><title>Service</title>
|
||||
<polygon fill="#d1ebf1" stroke="#1f477d" points="580,-65 486,-65 486,-25 580,-25 580,-65"/>
|
||||
<text text-anchor="middle" x="533" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">OpenStack</text>
|
||||
<text text-anchor="middle" x="533" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Service</text>
|
||||
</g>
|
||||
<!-- AuthComp->Service -->
|
||||
<g id="edge5" class="edge"><title>AuthComp->Service</title>
|
||||
<path fill="none" stroke="black" d="M338.009,-49.0804C344.065,-49.4598 350.172,-49.7828 356,-50 405.743,-51.8535 418.259,-51.9103 468,-50 470.523,-49.9031 473.101,-49.7851 475.704,-49.6504"/>
|
||||
<polygon fill="black" stroke="black" points="476.03,-53.1374 485.807,-49.0576 475.62,-46.1494 476.03,-53.1374"/>
|
||||
<text text-anchor="middle" x="412" y="-68.4" font-family="Times,serif" font-size="14.00">Forward Requests</text>
|
||||
<text text-anchor="middle" x="412" y="-53.4" font-family="Times,serif" font-size="14.00">with Identiy Status</text>
|
||||
</g>
|
||||
<!-- Service->AuthComp -->
|
||||
<g id="edge7" class="edge"><title>Service->AuthComp</title>
|
||||
<path fill="none" stroke="black" d="M495.062,-24.9037C486.397,-21.2187 477.064,-17.9304 468,-16 419.314,-5.63183 404.743,-5.9037 356,-16 349.891,-17.2653 343.655,-19.116 337.566,-21.2803"/>
|
||||
<polygon fill="black" stroke="black" points="336.234,-18.0426 328.158,-24.9003 338.748,-24.5757 336.234,-18.0426"/>
|
||||
<text text-anchor="middle" x="412" y="-33.4" font-family="Times,serif" font-size="14.00">Send Response OR</text>
|
||||
<text text-anchor="middle" x="412" y="-18.4" font-family="Times,serif" font-size="14.00">Reject Message</text>
|
||||
</g>
|
||||
<!-- Start -->
|
||||
<!-- Start->AuthComp -->
|
||||
<g id="edge9" class="edge"><title>Start->AuthComp</title>
|
||||
<path fill="none" stroke="black" d="M59.0178,-20.8384C99.2135,-25.0613 175.782,-33.1055 229.492,-38.7482"/>
|
||||
<polygon fill="black" stroke="black" points="229.265,-42.2435 239.576,-39.8076 229.997,-35.2818 229.265,-42.2435"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
309
doc/source/middlewarearchitecture.rst
Normal file
309
doc/source/middlewarearchitecture.rst
Normal file
@@ -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 = <path to middleware public cert>
|
||||
;keyfile = <path to middleware private cert>
|
||||
|
||||
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/.
|
||||
@@ -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)
|
||||
|
||||
157
keystoneclient/middleware/memcache_crypt.py
Executable file
157
keystoneclient/middleware/memcache_crypt.py
Executable file
@@ -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
|
||||
@@ -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):
|
||||
|
||||
56
tests/test_memcache_crypt.py
Normal file
56
tests/test_memcache_crypt.py
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user