migrate openstack hook to opnfv 35/935/1
authorWuKong <rebirthmonkey@gmail.com>
Wed, 1 Jul 2015 06:54:55 +0000 (08:54 +0200)
committerWuKong <rebirthmonkey@gmail.com>
Wed, 1 Jul 2015 06:54:55 +0000 (08:54 +0200)
Change-Id: I1e828dae38820fdff93966e57691b344af01140f
Signed-off-by: WuKong <rebirthmonkey@gmail.com>
102 files changed:
keystonemiddleware-moon/CONTRIBUTING.rst [new file with mode: 0644]
keystonemiddleware-moon/HACKING.rst [new file with mode: 0644]
keystonemiddleware-moon/LICENSE [new file with mode: 0644]
keystonemiddleware-moon/MANIFEST.in [new file with mode: 0644]
keystonemiddleware-moon/README.rst [new file with mode: 0644]
keystonemiddleware-moon/babel.cfg [new file with mode: 0644]
keystonemiddleware-moon/doc/.gitignore [new file with mode: 0644]
keystonemiddleware-moon/doc/Makefile [new file with mode: 0644]
keystonemiddleware-moon/doc/ext/__init__.py [new file with mode: 0644]
keystonemiddleware-moon/doc/ext/apidoc.py [new file with mode: 0644]
keystonemiddleware-moon/doc/source/audit.rst [new file with mode: 0644]
keystonemiddleware-moon/doc/source/conf.py [new file with mode: 0644]
keystonemiddleware-moon/doc/source/images/audit.png [new file with mode: 0644]
keystonemiddleware-moon/doc/source/images/graphs_authComp.svg [new file with mode: 0644]
keystonemiddleware-moon/doc/source/images/graphs_authCompDelegate.svg [new file with mode: 0644]
keystonemiddleware-moon/doc/source/index.rst [new file with mode: 0644]
keystonemiddleware-moon/doc/source/middlewarearchitecture.rst [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/certs/cacert.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/certs/middleware.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/certs/signing_cert.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/certs/ssl_cert.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_revoked.json [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_revoked.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_revoked.pkiz [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_scoped.json [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_scoped.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_scoped.pkiz [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_scoped_expired.json [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_scoped_expired.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_scoped_expired.pkiz [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_unscoped.json [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_unscoped.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_token_unscoped.pkiz [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_v3_token_revoked.json [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_v3_token_revoked.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_v3_token_revoked.pkiz [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_v3_token_scoped.json [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_v3_token_scoped.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/auth_v3_token_scoped.pkiz [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/revocation_list.der [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/revocation_list.json [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/revocation_list.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/cms/revocation_list.pkiz [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/gen_cmsz.py [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/gen_pki.sh [new file with mode: 0755]
keystonemiddleware-moon/examples/pki/private/cakey.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/private/signing_key.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/private/ssl_key.pem [new file with mode: 0644]
keystonemiddleware-moon/examples/pki/run_all.sh [new file with mode: 0755]
keystonemiddleware-moon/keystonemiddleware.egg-info/PKG-INFO [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware.egg-info/SOURCES.txt [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware.egg-info/dependency_links.txt [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware.egg-info/entry_points.txt [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware.egg-info/not-zip-safe [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware.egg-info/pbr.json [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware.egg-info/requires.txt [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware.egg-info/top_level.txt [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/__init__.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/audit.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/__init__.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_auth.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_base.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_cache.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_exceptions.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_identity.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_crypt.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_pool.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_revocations.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_signing_dir.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_user_plugin.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/auth_token/_utils.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/authz.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/ec2_token.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/i18n.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/openstack/__init__.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/openstack/common/__init__.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/openstack/common/memorycache.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/opts.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/s3_token.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/__init__.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/__init__.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/__init__.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_auth.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_connection_pool.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_memcache_crypt.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_revocations.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_signing_dir.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_utils.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/client_fixtures.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/test_audit_middleware.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/test_opts.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/test_s3_token_middleware.py [new file with mode: 0644]
keystonemiddleware-moon/keystonemiddleware/tests/unit/utils.py [new file with mode: 0644]
keystonemiddleware-moon/openstack-common.conf [new file with mode: 0644]
keystonemiddleware-moon/requirements.txt [new file with mode: 0644]
keystonemiddleware-moon/setup.cfg [new file with mode: 0644]
keystonemiddleware-moon/setup.py [new file with mode: 0644]
keystonemiddleware-moon/test-requirements-py3.txt [new file with mode: 0644]
keystonemiddleware-moon/test-requirements.txt [new file with mode: 0644]
keystonemiddleware-moon/tools/install_venv_common.py [new file with mode: 0644]
keystonemiddleware-moon/tox.ini [new file with mode: 0644]

diff --git a/keystonemiddleware-moon/CONTRIBUTING.rst b/keystonemiddleware-moon/CONTRIBUTING.rst
new file mode 100644 (file)
index 0000000..ba308f2
--- /dev/null
@@ -0,0 +1,16 @@
+If you would like to contribute to the development of OpenStack,
+you must follow the steps in this page:
+
+   http://docs.openstack.org/infra/manual/developers.html
+
+Once those steps have been completed, changes to OpenStack
+should be submitted for review via the Gerrit tool, following
+the workflow documented at:
+
+   http://docs.openstack.org/infra/manual/developers.html#development-workflow
+
+Pull requests submitted through GitHub will be ignored.
+
+Bugs should be filed on Launchpad, not GitHub:
+
+   https://bugs.launchpad.net/keystonemiddleware
diff --git a/keystonemiddleware-moon/HACKING.rst b/keystonemiddleware-moon/HACKING.rst
new file mode 100644 (file)
index 0000000..77de6b3
--- /dev/null
@@ -0,0 +1,24 @@
+Keystone Style Commandments
+===========================
+
+- Step 1: Read the OpenStack Style Commandments
+  http://docs.openstack.org/developer/hacking/
+- Step 2: Read on
+
+Exceptions
+----------
+
+When dealing with exceptions from underlying libraries, translate those
+exceptions to an instance or subclass of ClientException.
+
+=======
+Testing
+=======
+
+Keystone Middleware uses testtools and testr for its unittest suite
+and its test runner. Basic workflow around our use of tox and testr can
+be found at http://wiki.openstack.org/testr. If you'd like to learn more
+in depth:
+
+  https://testtools.readthedocs.org/
+  https://testrepository.readthedocs.org/
diff --git a/keystonemiddleware-moon/LICENSE b/keystonemiddleware-moon/LICENSE
new file mode 100644 (file)
index 0000000..32b6611
--- /dev/null
@@ -0,0 +1,209 @@
+Copyright (c) 2009 Jacob Kaplan-Moss - initial codebase (< v2.1)
+Copyright (c) 2011 Rackspace - OpenStack extensions (>= v2.1)
+Copyright (c) 2011 Nebula, Inc - Keystone refactor (>= v2.7)
+All rights reserved.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+--- License for python-keystoneclient versions prior to 2.1 ---
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+    3. Neither the name of this project nor the names of its contributors may
+    be used to endorse or promote products derived from this software without
+    specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/keystonemiddleware-moon/MANIFEST.in b/keystonemiddleware-moon/MANIFEST.in
new file mode 100644 (file)
index 0000000..29c0676
--- /dev/null
@@ -0,0 +1,7 @@
+include README.rst
+include AUTHORS HACKING LICENSE
+include ChangeLog
+include run_tests.sh tox.ini
+recursive-include doc *
+recursive-include tests *
+recursive-include tools *
diff --git a/keystonemiddleware-moon/README.rst b/keystonemiddleware-moon/README.rst
new file mode 100644 (file)
index 0000000..28781c5
--- /dev/null
@@ -0,0 +1,22 @@
+Middleware for the OpenStack Identity API (Keystone)
+====================================================
+
+This package contains middleware modules designed to provide authentication and
+authorization features to web services other than `Keystone
+<https://github.com/openstack/keystone>`. The most prominent module is
+``keystonemiddleware.auth_token``. This package does not expose any CLI or
+Python API features.
+
+The source is available on GitHub at:
+
+    http://github.com/openstack/keystonemiddleware
+
+Bugs and feature requests are tracked on Launchpad at:
+
+    https://bugs.launchpad.net/keystonemiddleware
+
+For any other information, refer to the parent project, Keystone:
+
+    https://github.com/openstack/keystone
+
+For information on contributing, see ``CONTRIBUTING.rst``.
diff --git a/keystonemiddleware-moon/babel.cfg b/keystonemiddleware-moon/babel.cfg
new file mode 100644 (file)
index 0000000..79cd39b
--- /dev/null
@@ -0,0 +1,3 @@
+[python: **.py]
+
+
diff --git a/keystonemiddleware-moon/doc/.gitignore b/keystonemiddleware-moon/doc/.gitignore
new file mode 100644 (file)
index 0000000..edde218
--- /dev/null
@@ -0,0 +1,2 @@
+build/
+source/api/
diff --git a/keystonemiddleware-moon/doc/Makefile b/keystonemiddleware-moon/doc/Makefile
new file mode 100644 (file)
index 0000000..84f00bd
--- /dev/null
@@ -0,0 +1,90 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+SPHINXSOURCE  = source
+PAPER         =
+BUILDDIR      = build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE)
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+       @echo "Please use \`make <target>' where <target> is one of"
+       @echo "  html      to make standalone HTML files"
+       @echo "  dirhtml   to make HTML files named index.html in directories"
+       @echo "  pickle    to make pickle files"
+       @echo "  json      to make JSON files"
+       @echo "  htmlhelp  to make HTML files and a HTML help project"
+       @echo "  qthelp    to make HTML files and a qthelp project"
+       @echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+       @echo "  changes   to make an overview of all changed/added/deprecated items"
+       @echo "  linkcheck to check all external links for integrity"
+       @echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+       -rm -rf $(BUILDDIR)/*
+
+html:
+       $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+       @echo
+       @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+       $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+       @echo
+       @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+pickle:
+       $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+       @echo
+       @echo "Build finished; now you can process the pickle files."
+
+json:
+       $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+       @echo
+       @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+       $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+       @echo
+       @echo "Build finished; now you can run HTML Help Workshop with the" \
+             ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+       $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+       @echo
+       @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+             ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+       @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/keystonemiddleware.qhcp"
+       @echo "To view the help file:"
+       @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/keystonemiddleware.qhc"
+
+latex:
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+       @echo
+       @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+       @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+             "run these through (pdf)latex."
+
+changes:
+       $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+       @echo
+       @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+       $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+       @echo
+       @echo "Link check complete; look for any errors in the above output " \
+             "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+       $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+       @echo "Testing of doctests in the sources finished, look at the " \
+             "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/keystonemiddleware-moon/doc/ext/__init__.py b/keystonemiddleware-moon/doc/ext/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/keystonemiddleware-moon/doc/ext/apidoc.py b/keystonemiddleware-moon/doc/ext/apidoc.py
new file mode 100644 (file)
index 0000000..2575f42
--- /dev/null
@@ -0,0 +1,46 @@
+# Copyright 2014 OpenStack Foundation
+#
+# 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.
+
+# NOTE(blk-u): Uncomment the [pbr] section in setup.cfg and remove this
+# Sphinx extension when https://launchpad.net/bugs/1260495 is fixed.
+
+import os.path as path
+
+from sphinx import apidoc
+
+
+# NOTE(blk-u): pbr will run Sphinx multiple times when it generates
+# documentation. Once for each builder. To run this extension we use the
+# 'builder-inited' hook that fires at the beginning of a Sphinx build.
+# We use ``run_already`` to make sure apidocs are only generated once
+# even if Sphinx is run multiple times.
+run_already = False
+
+
+def run_apidoc(app):
+    global run_already
+    if run_already:
+        return
+    run_already = True
+
+    package_dir = path.abspath(path.join(app.srcdir, '..', '..',
+                                         'keystonemiddleware'))
+    source_dir = path.join(app.srcdir, 'api')
+    apidoc.main(['apidoc', package_dir, '-f',
+                 '-H', 'keystonemiddleware Modules',
+                 '-o', source_dir])
+
+
+def setup(app):
+    app.connect('builder-inited', run_apidoc)
diff --git a/keystonemiddleware-moon/doc/source/audit.rst b/keystonemiddleware-moon/doc/source/audit.rst
new file mode 100644 (file)
index 0000000..d23f816
--- /dev/null
@@ -0,0 +1,81 @@
+..
+      Copyright 2014 IBM Corp
+
+      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:
+
+=================
+ Audit middleware
+=================
+
+The Keystone middleware library provides an optional WSGI middleware filter
+which allows the ability to audit API requests for each component of OpenStack.
+
+The audit middleware filter utilises environment variables to build the CADF
+event.
+
+.. figure:: ./images/audit.png
+   :width: 100%
+   :align: center
+   :alt: Figure 1: Audit middleware in Nova pipeline
+
+The figure above shows the middleware in Nova's pipeline.
+
+Enabling audit middleware
+=========================
+To enable auditing, oslo.messaging_ should be installed. If not, the middleware
+will log the audit event instead. Auditing can be enabled for a specific
+project by editing the project's api-paste.ini file to include the following
+filter definition:
+
+::
+
+   [filter:audit]
+   paste.filter_factory = keystonemiddleware.audit:filter_factory
+   audit_map_file = /etc/nova/api_audit_map.conf
+
+The filter should be included after Keystone middleware's auth_token middleware
+so it can utilise environment variables set by auth_token. Below is an example
+using Nova's WSGI pipeline::
+
+   [composite:openstack_compute_api_v2]
+   use = call:nova.api.auth:pipeline_factory
+   noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2
+   keystone = faultwrap sizelimit authtoken keystonecontext ratelimit audit osapi_compute_app_v2
+   keystone_nolimit = faultwrap sizelimit authtoken keystonecontext audit osapi_compute_app_v2
+
+.. _oslo.messaging: http://www.github.com/openstack/oslo.messaging
+
+Configure audit middleware
+==========================
+To properly audit api requests, the audit middleware requires an
+api_audit_map.conf to be defined. The project's corresponding
+api_audit_map.conf file is included in the `pyCADF library`_.
+
+The location of the mapping file should be specified explicitly by adding the
+path to the 'audit_map_file' option of the filter definition::
+
+   [filter:audit]
+   paste.filter_factory = keystonemiddleware.audit:filter_factory
+   audit_map_file = /etc/nova/api_audit_map.conf
+
+Additional options can be set::
+
+   [filter:audit]
+   paste.filter_factory = pycadf.middleware.audit:filter_factory
+   audit_map_file = /etc/nova/api_audit_map.conf
+   service_name = test # opt to set HTTP_X_SERVICE_NAME environ variable
+   ignore_req_list = GET,POST # opt to ignore specific requests
+
+.. _pyCADF library: https://github.com/openstack/pycadf/tree/master/etc/pycadf
diff --git a/keystonemiddleware-moon/doc/source/conf.py b/keystonemiddleware-moon/doc/source/conf.py
new file mode 100644 (file)
index 0000000..069382b
--- /dev/null
@@ -0,0 +1,237 @@
+# -*- coding: utf-8 -*-
+#
+# keystonemiddleware documentation build configuration file, created by
+# sphinx-quickstart on Sun Dec  6 14:19:25 2009.
+#
+# This file is execfile()d with the current directory set to its containing
+# dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+from __future__ import unicode_literals
+
+import os
+import sys
+
+import pbr.version
+
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),
+                '..', '..')))
+
+# NOTE(blk-u): Path for our Sphinx extension, remove when
+# https://launchpad.net/bugs/1260495 is fixed.
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),
+                '..')))
+
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# -- General configuration ----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc',
+              'sphinx.ext.todo',
+              'sphinx.ext.coverage',
+              'sphinx.ext.intersphinx',
+              # NOTE(blk-u): Uncomment the [pbr] section in setup.cfg and
+              # remove this Sphinx extension when
+              # https://launchpad.net/bugs/1260495 is fixed.
+              'ext.apidoc',
+              'oslosphinx'
+             ]
+
+todo_include_todos = True
+
+# Add any paths that contain templates here, relative to this directory.
+#templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'keystonemiddleware'
+copyright = 'OpenStack Contributors'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+version_info = pbr.version.VersionInfo('keystonemiddleware')
+# The short X.Y version.
+version = version_info.version_string()
+# The full version, including alpha/beta/rc tags.
+release = version_info.release_string()
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = []
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# Grouping the document tree for man pages.
+# List of tuples 'sourcefile', 'target', 'title', 'Authors name', 'manual'
+
+man_pages = []
+
+# -- Options for HTML output --------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+#html_theme_path = ["."]
+#html_theme = '_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+#html_static_path = ['static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1"
+html_last_updated_fmt = os.popen(git_cmd).read()
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'keystonemiddlewaredoc'
+
+
+# -- Options for LaTeX output -------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual])
+# .
+latex_documents = [
+    ('index', 'keystonmiddleware.tex',
+     'keystonemiddleware Documentation',
+     'Nebula Inc, based on work by Rackspace and Jacob Kaplan-Moss',
+     'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+keystoneclient = 'http://docs.openstack.org/developer/python-keystoneclient/'
+
+intersphinx_mapping = {'keystoneclient': (keystoneclient, None),
+                       }
diff --git a/keystonemiddleware-moon/doc/source/images/audit.png b/keystonemiddleware-moon/doc/source/images/audit.png
new file mode 100644 (file)
index 0000000..5c2b130
Binary files /dev/null and b/keystonemiddleware-moon/doc/source/images/audit.png differ
diff --git a/keystonemiddleware-moon/doc/source/images/graphs_authComp.svg b/keystonemiddleware-moon/doc/source/images/graphs_authComp.svg
new file mode 100644 (file)
index 0000000..6be629c
--- /dev/null
@@ -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&#45;&gt;Reject -->
+<g id="edge3" class="edge"><title>AuthComp&#45;&gt;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&#45;&gt;Service -->
+<g id="edge5" class="edge"><title>AuthComp&#45;&gt;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&#45;&gt;AuthComp -->
+<g id="edge7" class="edge"><title>Start&#45;&gt;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>
diff --git a/keystonemiddleware-moon/doc/source/images/graphs_authCompDelegate.svg b/keystonemiddleware-moon/doc/source/images/graphs_authCompDelegate.svg
new file mode 100644 (file)
index 0000000..4788829
--- /dev/null
@@ -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&#45;&gt;Reject -->
+<g id="edge3" class="edge"><title>AuthComp&#45;&gt;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&#45;&gt;Service -->
+<g id="edge5" class="edge"><title>AuthComp&#45;&gt;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&#45;&gt;AuthComp -->
+<g id="edge7" class="edge"><title>Service&#45;&gt;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&#45;&gt;AuthComp -->
+<g id="edge9" class="edge"><title>Start&#45;&gt;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>
diff --git a/keystonemiddleware-moon/doc/source/index.rst b/keystonemiddleware-moon/doc/source/index.rst
new file mode 100644 (file)
index 0000000..9092ec7
--- /dev/null
@@ -0,0 +1,46 @@
+Python Middleware for OpenStack Identity API (Keystone)
+=======================================================
+
+This is the middleware provided for integrating with the OpenStack
+Identity API and handling authorization enforcement based upon the
+data within the OpenStack Identity tokens. Also included is middleware that
+provides the ability to create audit events based on API requests.
+
+Contents:
+
+.. toctree::
+   :maxdepth: 1
+
+   middlewarearchitecture
+   audit
+
+Related Identity Projects
+=========================
+
+In addition to creating the Python Middleware for OpenStack Identity
+API, the Keystone team also provides `Identity Service`_, as well as
+`Python Client Library`_.
+
+.. _`Identity Service`: http://docs.openstack.org/developer/keystone/
+.. _`Python Client Library`: http://docs.openstack.org/developer/python-keystoneclient/
+
+Contributing
+============
+
+Code is hosted `on GitHub`_. Submit bugs to the Keystone project on
+`Launchpad`_. Submit code to the ``openstack/keystonemiddleware`` project
+using `Gerrit`_.
+
+.. _on GitHub: https://github.com/openstack/keystonemiddleware
+.. _Launchpad: https://launchpad.net/keystonemiddleware
+.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow
+
+Run tests with ``python setup.py test``.
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/keystonemiddleware-moon/doc/source/middlewarearchitecture.rst b/keystonemiddleware-moon/doc/source/middlewarearchitecture.rst
new file mode 100644 (file)
index 0000000..e02aad4
--- /dev/null
@@ -0,0 +1,459 @@
+..
+      Copyright 2011-2013 OpenStack Foundation
+      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 mechanism, 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.
+
+This documentation describes the implementation in
+:class:`keystonemiddleware.auth_token`
+
+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
+  * if invalid, or no 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 operate in a 'delegated mode'.
+In this mode, the decision to 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 Oslo 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:
+
+.. code-block:: ini
+
+    [app:myService]
+    paste.app_factory = myService:app_factory
+
+    [pipeline:main]
+    pipeline = authtoken myService
+
+    [filter:authtoken]
+    paste.filter_factory = keystonemiddleware.auth_token:filter_factory
+
+    # Prefix to prepend at the beginning of the path (string
+    # value)
+    #auth_admin_prefix=
+
+    # Host providing the admin Identity API endpoint (string
+    # value)
+    auth_host=127.0.0.1
+
+    # Port of the admin Identity API endpoint (integer value)
+    auth_port=35357
+
+    # Protocol of the admin Identity API endpoint(http or https)
+    # (string value)
+    auth_protocol=https
+
+    # Complete public Identity API endpoint (string value)
+    #auth_uri=<None>
+
+    # API version of the admin Identity API endpoint (string
+    # value)
+    #auth_version=<None>
+
+    # Do not handle authorization requests within the middleware,
+    # but delegate the authorization decision to downstream WSGI
+    # components (boolean value)
+    #delay_auth_decision=false
+
+    # Request timeout value for communicating with Identity API
+    # server. (boolean value)
+    #http_connect_timeout=<None>
+
+    # How many times are we trying to reconnect when communicating
+    # with Identity API Server. (integer value)
+    #http_request_max_retries=3
+
+    # Single shared secret with the Keystone configuration used
+    # for bootstrapping a Keystone installation, or otherwise
+    # bypassing the normal authentication process. (string value)
+    #admin_token=<None>
+
+    # Keystone account username (string value)
+    #admin_user=<None>
+
+    # Keystone account password (string value)
+    admin_password=SuperSekretPassword
+
+    # Keystone service account tenant name to validate user tokens
+    # (string value)
+    #admin_tenant_name=admin
+
+    # Env key for the swift cache (string value)
+    #cache=<None>
+
+    # Required if Keystone server requires client certificate
+    # (string value)
+    #certfile=<None>
+
+    # Required if Keystone server requires client certificate
+    # (string value)
+    #keyfile=<None>
+
+    # A PEM encoded Certificate Authority to use when verifying
+    # HTTPs connections. Defaults to system CAs. (string value)
+    #cafile=<None>
+
+    # Verify HTTPS connections. (boolean value)
+    #insecure=false
+
+    # Directory used to cache files related to PKI tokens (string
+    # value)
+    #signing_dir=<None>
+
+    # If defined, the memcache server(s) to use for caching (list
+    # value)
+    # Deprecated group/name - [DEFAULT]/memcache_servers
+    #memcached_servers=<None>
+
+    # In order to prevent excessive requests and validations, the
+    # middleware uses an in-memory cache for the tokens the
+    # Keystone API returns. This is only valid if memcache_servers
+    # is defined. Set to -1 to disable caching completely.
+    # (integer value)
+    #token_cache_time=300
+
+    # Value only used for unit testing (integer value)
+    #revocation_cache_time=1
+
+    # (optional) if defined, indicate whether token data should be
+    # authenticated or authenticated and encrypted. Acceptable
+    # values are MAC or ENCRYPT.  If MAC, token data is
+    # authenticated (with HMAC) in the cache. If ENCRYPT, token
+    # data is encrypted and authenticated in the cache. If the
+    # value is not one of these options or empty, auth_token will
+    # raise an exception on initialization. (string value)
+    #memcache_security_strategy=<None>
+
+    # (optional, mandatory if memcache_security_strategy is
+    # defined) this string is used for key derivation. (string
+    # value)
+    #memcache_secret_key=<None>
+
+    # (optional) indicate whether to set the X-Service-Catalog
+    # header. If False, middleware will not ask for service
+    # catalog on token validation and will not set the X-Service-
+    # Catalog header. (boolean value)
+    #include_service_catalog=true
+
+    # Used to control the use and type of token binding. Can be
+    # set to: "disabled" to not check token binding. "permissive"
+    # (default) to validate binding information if the bind type
+    # is of a form known to the server and ignore it if not.
+    # "strict" like "permissive" but if the bind type is unknown
+    # the token will be rejected. "required" any form of token
+    # binding is needed to be allowed. Finally the name of a
+    # binding method that must be present in tokens. (string
+    # value)
+    #enforce_token_bind=permissive
+
+For services which have a 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``:
+
+.. code-block:: ini
+
+    [filter:authtoken]
+    paste.filter_factory = keystonemiddleware.auth_token:filter_factory
+
+and set in ``nova.conf``:
+
+.. code-block:: ini
+
+    [DEFAULT]
+    auth_strategy=keystone
+
+    [keystone_authtoken]
+    auth_host = 127.0.0.1
+    auth_port = 35357
+    auth_protocol = http
+    admin_user = admin
+    admin_password = SuperSekretPassword
+    admin_tenant_name = service
+    # Any of the options that could be set in api-paste.ini can be set here.
+
+Note that middleware parameters in paste config take priority, they must be
+removed to use values in [keystone_authtoken] section.
+
+Configuration Options
+---------------------
+
+* ``auth_admin_prefix``: Prefix to prepend at the beginning of the path
+* ``auth_host``: (required) the host providing the keystone service API endpoint
+  for validating and requesting tokens
+* ``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`)
+* ``auth_version``: API version of the admin Identity API endpoint
+* ``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.
+* ``http_connect_timeout``: (optional) Request timeout value for communicating
+  with Identity API server.
+* ``http_request_max_retries``: (default 3) How many times are we trying to
+  reconnect when communicating with Identity API Server.
+* ``http_handler``: (optional) Allows to pass in the name of a fake
+  http_handler callback function used instead of `httplib.HTTPConnection` or
+  `httplib.HTTPSConnection`. Useful for unit testing where network is not
+  available.
+
+* ``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.
+
+* ``cache``: (optional) Env key for the swift cache
+
+* ``certfile``: (required, if Keystone server requires client cert)
+* ``keyfile``: (required, if Keystone server requires client cert)  This can be
+  the same as the certfile if the certfile includes the private key.
+* ``cafile``: (optional, defaults to use system CA bundle) the path to a PEM
+  encoded CA file/bundle that will be used to verify HTTPS connections.
+* ``insecure``: (optional, default `False`) Don't verify HTTPS connections
+  (overrides `cafile`).
+
+* ``signing_dir``: (optional) Directory used to cache files related to PKI
+  tokens
+
+* ``memcached_servers``: (optional) If defined, the memcache server(s) to use
+  for caching
+* ``token_cache_time``: (default 300) In order to prevent excessive requests
+  and validations, the middleware uses an in-memory cache for the tokens the
+  Keystone API returns. This is only valid if memcache_servers s defined. Set
+  to -1 to disable caching completely.
+* ``memcache_security_strategy``: (optional) if defined, indicate whether token
+  data should be authenticated or authenticated and encrypted. Acceptable
+  values are MAC or ENCRYPT.  If MAC, token data is authenticated (with HMAC)
+  in the cache. If ENCRYPT, token data is encrypted and authenticated in the
+  cache. If the value is not one of these options or empty, auth_token will
+  raise an exception on initialization.
+* ``memcache_secret_key``: (mandatory if memcache_security_strategy is defined)
+   this string is used for key derivation.
+* ``include_service_catalog``: (optional, default `True`) Indicate whether to
+  set the X-Service-Catalog header. If False, middleware will not ask for
+  service catalog on token validation and will not set the X-Service-Catalog
+  header.
+* ``enforce_token_bind``: (default ``permissive``) Used to control the use and
+  type of token binding. Can be set to: "disabled" to not check token binding.
+  "permissive" (default) to validate binding information if the bind type is of
+  a form known to the server and ignore it if not. "strict" like "permissive"
+  but if the bind type is unknown the token will be rejected. "required" any
+  form of token binding is needed to be allowed. Finally the name of a binding
+  method that must be present in tokens.
+
+Caching for improved response
+-----------------------------
+
+In order to prevent excessive requests and validations, the middleware uses an
+in-memory cache for the tokens the keystone API returns. Keep in mind that
+invalidated tokens may continue to work if they are still in the token cache,
+so token_cache_time is configurable. For larger deployments, the middleware
+also supports memcache based caching.
+
+* ``memcached_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) Set to -1 to disable
+  caching completely.
+
+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 dependencies
+======================
+
+In order to use `memcached`_ it is necessary to install the `python-memcached`_
+library. If data stored in `memcached`_ will need to be encrypted it is also
+necessary to install the `pycrypto`_ library. These libs are not listed in
+the requirements.txt file.
+
+.. _`memcached`: http://memcached.org/
+.. _`python-memcached`: https://pypi.python.org/pypi/python-memcached
+.. _`pycrypto`: https://pypi.python.org/pypi/pycrypto
+
+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.
+
+Memcache Protection
+===================
+
+When using memcached, we are storing user tokens and token validation
+information into the cache as raw data. Which means that anyone who
+has access to the memcache servers can read and modify data stored
+there. To mitigate this risk, ``auth_token`` middleware provides an
+option to authenticate and optionally encrypt the token data stored in
+the cache.
+
+* ``memcache_security_strategy``: (optional) if defined, indicate
+  whether token data should be authenticated or authenticated and
+  encrypted. Acceptable values are ``MAC`` or ``ENCRYPT``. If ``MAC``,
+  token data is authenticated (with HMAC) in the cache. If
+  ``ENCRYPT``, token data is encrypted and authenticated in the
+  cache. If the value is not one of these options or empty,
+  ``auth_token`` will raise an exception on initialization.
+* ``memcache_secret_key``: (optional, mandatory if
+  ``memcache_security_strategy`` is defined) this string is 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.
+
+An ``X-Service-Token`` header may also be included with a request. If present,
+and the value of ``X-Auth-Token`` or ``X-Storage-Token`` has not caused the
+request to be denied, then the middleware will attempt to validate the value of
+``X-Service-Token``. If valid, the authentication middleware extends the HTTP
+request with the header ``X-Service-Identity-Status`` having value `Confirmed`
+and also extends the request with additional headers representing the identity
+authenticated and authorised by the token.
+
+If ``X-Service-Token`` is present and its value is invalid and the
+``delay_auth_decision`` option is True then the value of
+``X-Service-Identity-Status`` is set to `Invalid` and no further headers are
+added. Otherwise if ``X-Service-Token`` is present and its value is invalid
+then the middleware will respond to the HTTP request with HTTPUnauthorized,
+regardless of the validity of the ``X-Auth-Token`` or ``X-Storage-Token``
+values.
+
+Extended the request with additional User Information
+-----------------------------------------------------
+
+:py:class:`keystonemiddleware.auth_token.AuthProtocol` extends the
+request with additional information if the user has been authenticated. See the
+"What we add to the request for use by the OpenStack service" section in
+:py:mod:`keystonemiddleware.auth_token` for the list of fields set by
+the auth_token middleware.
+
+
+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/keystonemiddleware-moon/examples/pki/certs/cacert.pem b/keystonemiddleware-moon/examples/pki/certs/cacert.pem
new file mode 100644 (file)
index 0000000..952bdae
--- /dev/null
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID1jCCAr6gAwIBAgIJAJOtRP2+wrM/MA0GCSqGSIb3DQEBBQUAMIGeMQowCAYD
+VQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55
+dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMG
+CSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2Vs
+ZiBTaWduZWQwIBcNMTMwOTEzMTYyNTQyWhgPMjA3MjAzMDcxNjI1NDJaMIGeMQow
+CAYDVQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1
+bm55dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTEl
+MCMGCSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxML
+U2VsZiBTaWduZWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCl8906
+EaRpibQFcCBWfxzLi5x/XpZ9iL6UX92NrSJxcDbaGws7s+GtjgDy8UOEonesRWTe
+qQEZtHpC3/UHHOnsA8F6ha/pq9LioqT7RehCnZCLBJwh5Ct+lclpWs15SkjJD2LT
+Dkjox0eA9nOBx+XDlWyU/GAyqx5Wsvg/Kxr0iod9/4IcJdnSdUjq4v0Cxg/zNk08
+XPJX+F0bUDhgdUf7JrAmmS5LA8wphRnbIgtVsf6VN9HrbqtHAJDxh8gEfuwdhEW1
+df1fBtZ+6WMIF3IRSbIsZELFB6sqcyRj7HhMoWMkdEyPb2f8mq61MzTgE6lJGIyT
+RvEoFie7qtGADIofAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN
+AQEFBQADggEBAJRMdEwAdN+crqI9dBLYlbBbnQ8xr9mk+REMdz9+SKhDCNdVisWU
+iLEZvK/aozrsRsDi81JjS4Tz0wXo8zsPPoDnXgDYEicNPTKifbPKgHdDIGFOwBKn
+y2cF6fHEn8n3KIBrDCNY6rHcYGZ7lbq/8eF0GoYQboPiuYesvVpynPmIK5/Mmire
+EuuZALAe1IFqqFt+l6tiJU2JWUFjLkFARMOD14qFZm+SInl64toi08j6gdou+NMW
+7GEMbVHwNTafM/TgFN5j0yP9SAnYubckLSyH6hwR+rM8dztP5769joxQfnc9O/Bn
+TBD9KFpeQv6VJWLAxiIKcQCRTTDJLZZ0MQI=
+-----END CERTIFICATE-----
diff --git a/keystonemiddleware-moon/examples/pki/certs/middleware.pem b/keystonemiddleware-moon/examples/pki/certs/middleware.pem
new file mode 100644 (file)
index 0000000..7d593ef
--- /dev/null
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+MIIDpjCCAo4CARAwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK
+EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr
+ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x
+MzA5MTMxNjI1NDNaGA8yMDcyMDMwNzE2MjU0M1owgZAxCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh
+Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv
+cGVuc3RhY2sub3JnMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDL06AaJROwHPgJ9tcySSBepzJ81jYars2sMvLjyuvd
+iIBbhWvbS/a9Tw3WgL8H6OALkHiOU/f0A6Rpv8dGDIDsxZQVjT/4SLaQUOeDM+9b
+fkKHpSd9G3CsdSSZgOH08n+MyZ7slPHfUHLYWso0SJD0vAi1gmGDlSM/mmhhHTpC
+DGo6Wbwqare6JNeTCGJTJYwrxtoMCh/W1ZrslPC5lFvlHD7KBBf6IU2A8Xh/dUa3
+p5pmQeHPW8Em90DzIB1qH0DRXl3KANc24xYRR45pPCVkk6vFsy6P0JwwpnkszB+L
+cK6CEsJhLsOYvQFsiQfSZ8m7YGhgrMLxtop4YEPirGGrAgMBAAEwDQYJKoZIhvcN
+AQEFBQADggEBAAjU7YomUx/U56p1KWHvr1B7oczHF8fPHYbuk5c/N81WOJeSRy+P
+5ZGZ2UPjvqqXByv+78YWMKGY1BZ/2doeWuydr0sdSxEwmIUBYxFpujuYY+0AjS/n
+mMr1ZijK7TJssteKM7/MClzghUhPweDZrAg3ff1hbhK5QSy+9UPxUqLH44tfYSVC
+/BzM6se0p5ToM0bwdsa8TofaBRE1L1IW/Hg4VIGOoKs0R0uLm7+Oot2me2cEuZ6h
+Wls6MED8ND1Nz8EAKwndkeDu2iMM+qx/YFp6K8BQ5E5nXd2rbUZUlQMp1WbUlZ87
+KvC98aT0UYIq6uo1Lx/dQvJs7faAkYd4lmE=
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDL06AaJROwHPgJ
+9tcySSBepzJ81jYars2sMvLjyuvdiIBbhWvbS/a9Tw3WgL8H6OALkHiOU/f0A6Rp
+v8dGDIDsxZQVjT/4SLaQUOeDM+9bfkKHpSd9G3CsdSSZgOH08n+MyZ7slPHfUHLY
+Wso0SJD0vAi1gmGDlSM/mmhhHTpCDGo6Wbwqare6JNeTCGJTJYwrxtoMCh/W1Zrs
+lPC5lFvlHD7KBBf6IU2A8Xh/dUa3p5pmQeHPW8Em90DzIB1qH0DRXl3KANc24xYR
+R45pPCVkk6vFsy6P0JwwpnkszB+LcK6CEsJhLsOYvQFsiQfSZ8m7YGhgrMLxtop4
+YEPirGGrAgMBAAECggEATwvbY0hNwlb5uqOIAXBqpUqiQdexU9fG26lGmSDxKBDv
+9o5frcRgBDrMWwvDCgY+HT4CAvB9kJx4/qnpVjkzJp/ZNiJ5VIiehIlbv348rXbh
+xkk+bz5dDATCFOXuu1fwL2FhyM5anwhMAav0DyK1VLQ3jGzr9GO6L8hqAn+bQFFu
+6ngiODwfhBMl5aRoL9UOBEhccK07znrH0JGRz+3+5Cdz59Xw91Bv210LhNNDL58+
+0JD0N+YztVOQd2bgwo0bQbOEijzmYq+0mjoqAnJh1/++y7PlIPs0AnPgqSnFPx9+
+6FsQEVRgk5Uq3kvPLaP4nT2y6MDZSp+ujYldvJhyQQKBgQDuX2pZIJMZ4aFnkG+K
+TmJ5wsLa/u9an0TmvAL9RLtBpVpQNKD8cQ+y8PUZavXDbAIt5NWqZVnTbCR79Dnd
+mZKblwcHhtsyA5f89el5KcxY2BREWdHdTnJpNd7XRlUECmzvX1zGj77lA982PhII
+yflRBRV3vqLkgC8vfoYgRyRElwKBgQDa5jnLdx/RahfYMOgn1HE5o4hMzLR4Y0Dd
++gELshcUbPqouoP5zOb8WOagVJIgZVOSN+/VqbilVYrqRiNTn2rnoxs+HHRdaJNN
+3eXllD4J2HfC2BIj1xSpIdyh2XewAJqw9IToHNB29QUhxOtgwseHciPG6JaKH2ik
+kqGKH/EKDQKBgFFAftygiOPCkCTgC9UmANUmOQsy6N2H+pF3tsEj43xt44oBVnqW
+A1boYXNnjRwuvdNs9BPf9i1l6E3EItFRXrLgWQoMwryakv0ryYh+YeRKyyW9RBbe
+fYs1TJ8unx4Ae79gTxxztQsVNcmkgLs0NWKTjAzEE3w14V+cDhYEie1DAoGBAJdI
+V5cLrBzBstsB6eBlDR9lqrRRIUS2a8U9m+1mVlcSfiWQSdehSd4K3tDdwePLw3ch
+W4qR8n+pYAlLEe0gFvUhn5lMdwt7U5qUCeehjUKmrRYm2FqWsbu2IFJnBjXIJSC4
+zQXRrC0aZ0KQYpAL7XPpaVp1slyhGmPqxuO78Y0dAoGBAMHo3EIMwu9rfuGwFodr
+GFsOZhfJqgo5GDNxxf89Q9WWpMDTCdX+wdBTrN/wsMbBuwIDHrUuRnk6D5CWRjSk
+/ikCgHN3kOtrbL8zzqRomGAIIWKYGFEIGe1GHVGo5r//HXHdPxFXygvruQ/xbOA4
+RGvmDiji8vVDq7Shho8I6KuT
+-----END PRIVATE KEY-----
diff --git a/keystonemiddleware-moon/examples/pki/certs/signing_cert.pem b/keystonemiddleware-moon/examples/pki/certs/signing_cert.pem
new file mode 100644 (file)
index 0000000..63ab247
--- /dev/null
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDpTCCAo0CAREwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK
+EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr
+ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x
+MzA5MTMxNjI1NDNaGA8yMDcyMDMwNzE2MjU0M1owgY8xCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh
+Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv
+cGVuc3RhY2sub3JnMREwDwYDVQQDEwhLZXlzdG9uZTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAMz5WsgsuX3rZUdLwQpZXN2Ro7LQ6jEZnreBqMztVObw
+BuC1WdiJsg6dVlC7PVdt+0gY1c8WFg1TKmsucxesQSyfGAPg+9T/hsRMb6y12uJx
+fp3Wgqqw0U1HsXvMiaJH87MaGnt043BxzF+R9fhAcDk6Cyj5cx9J0LvZJEOzN4J4
+ZRyO6j/DZZItb3lK5W9xkuoT+mTdDZOQJnXyG818uiWfjdCkLjr1ruytRcBOo4na
+Y828voT/A7I95+YCgKgbjiUWhHeTaNmMEQiGy0nGYfteC+oSsHOlxZ3b12azzHPk
+83Bh2ez0Ih9vcZoe9DqvlFOXfv9q8OsYc5Yo6gPTXEsCAwEAATANBgkqhkiG9w0B
+AQUFAAOCAQEAmaYE98kOQWu6DV84ZcZP/OdT8eeu3vdB247nRj+6+GYItN/Gzqt4
+HVvz7c+FVTolCcAQQ+z3XGswI9fIJ78Hb0p9CgnLprc3L7Xtk60Im59Xlf3tcurn
+r/ZnSDcjRBXKiEDrSM0VrhAnc0GoSeb6aDWopec+1hWOWfBVAg9R8yJgU9sUgO3O
+0gimGyrw8eubmNhckSQLJTunUTsrkcBjuSg63wAD9OqCiX6c2eoQr+0YBp2eV2/n
+aOiJXWNLbeueMKSYiJNyyvM/dlON7/56cdwDTzKzgD34TImouM5VKipUwCX1ovLu
+ITLzALzpqFFzc8ugV9pMgUKtDbZoPp9EEA==
+-----END CERTIFICATE-----
diff --git a/keystonemiddleware-moon/examples/pki/certs/ssl_cert.pem b/keystonemiddleware-moon/examples/pki/certs/ssl_cert.pem
new file mode 100644 (file)
index 0000000..cdd2e4c
--- /dev/null
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDpjCCAo4CARAwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK
+EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr
+ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x
+MzA5MTMxNjI1NDNaGA8yMDcyMDMwNzE2MjU0M1owgZAxCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh
+Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv
+cGVuc3RhY2sub3JnMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDL06AaJROwHPgJ9tcySSBepzJ81jYars2sMvLjyuvd
+iIBbhWvbS/a9Tw3WgL8H6OALkHiOU/f0A6Rpv8dGDIDsxZQVjT/4SLaQUOeDM+9b
+fkKHpSd9G3CsdSSZgOH08n+MyZ7slPHfUHLYWso0SJD0vAi1gmGDlSM/mmhhHTpC
+DGo6Wbwqare6JNeTCGJTJYwrxtoMCh/W1ZrslPC5lFvlHD7KBBf6IU2A8Xh/dUa3
+p5pmQeHPW8Em90DzIB1qH0DRXl3KANc24xYRR45pPCVkk6vFsy6P0JwwpnkszB+L
+cK6CEsJhLsOYvQFsiQfSZ8m7YGhgrMLxtop4YEPirGGrAgMBAAEwDQYJKoZIhvcN
+AQEFBQADggEBAAjU7YomUx/U56p1KWHvr1B7oczHF8fPHYbuk5c/N81WOJeSRy+P
+5ZGZ2UPjvqqXByv+78YWMKGY1BZ/2doeWuydr0sdSxEwmIUBYxFpujuYY+0AjS/n
+mMr1ZijK7TJssteKM7/MClzghUhPweDZrAg3ff1hbhK5QSy+9UPxUqLH44tfYSVC
+/BzM6se0p5ToM0bwdsa8TofaBRE1L1IW/Hg4VIGOoKs0R0uLm7+Oot2me2cEuZ6h
+Wls6MED8ND1Nz8EAKwndkeDu2iMM+qx/YFp6K8BQ5E5nXd2rbUZUlQMp1WbUlZ87
+KvC98aT0UYIq6uo1Lx/dQvJs7faAkYd4lmE=
+-----END CERTIFICATE-----
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_revoked.json b/keystonemiddleware-moon/examples/pki/cms/auth_token_revoked.json
new file mode 100644 (file)
index 0000000..3da8f8b
--- /dev/null
@@ -0,0 +1,85 @@
+{
+    "access": {
+        "token": {
+            "expires": "2038-01-18T21:14:07Z",
+            "id": "placeholder",
+            "tenant": {
+                "id": "tenant_id1",
+                "enabled": true,
+                "description": null,
+                "name": "tenant_name1"
+            }
+        },
+        "serviceCatalog": [
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a"
+                    }
+                ],
+                "type": "volume",
+                "name": "volume"
+            },
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:9292/v1",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:9292/v1",
+                        "publicURL": "http://127.0.0.1:9292/v1"
+                    }
+                ],
+                "type": "image",
+                "name": "glance"
+            },
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a"
+                    }
+                ],
+                "type": "compute",
+                "name": "nova"
+            },
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:35357/v2.0",
+                        "region": "RegionOne",
+                        "internalURL": "http://127.0.0.1:35357/v2.0",
+                        "publicURL": "http://127.0.0.1:5000/v2.0"
+                    }
+                ],
+                "type": "identity",
+                "name": "keystone"
+            }
+        ],
+        "user": {
+            "username": "revoked_username1",
+            "roles_links": [
+                "role1",
+                "role2"
+            ],
+            "id": "revoked_user_id1",
+            "roles": [
+                {
+                    "name": "role1"
+                },
+                {
+                    "name": "role2"
+                }
+            ],
+            "name": "revoked_username1"
+        }
+    }
+}
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_revoked.pem b/keystonemiddleware-moon/examples/pki/cms/auth_token_revoked.pem
new file mode 100644 (file)
index 0000000..a685a45
--- /dev/null
@@ -0,0 +1,75 @@
+-----BEGIN CMS-----
+MIINnQYJKoZIhvcNAQcCoIINjjCCDYoCAQExCTAHBgUrDgMCGjCCC6oGCSqGSIb3
+DQEHAaCCC5sEgguXew0KICAgICJhY2Nlc3MiOiB7DQogICAgICAgICJ0b2tlbiI6
+IHsNCiAgICAgICAgICAgICJleHBpcmVzIjogIjIwMzgtMDEtMThUMjE6MTQ6MDda
+IiwNCiAgICAgICAgICAgICJpZCI6ICJwbGFjZWhvbGRlciIsDQogICAgICAgICAg
+ICAidGVuYW50Ijogew0KICAgICAgICAgICAgICAgICJpZCI6ICJ0ZW5hbnRfaWQx
+IiwNCiAgICAgICAgICAgICAgICAiZW5hYmxlZCI6IHRydWUsDQogICAgICAgICAg
+ICAgICAgImRlc2NyaXB0aW9uIjogbnVsbCwNCiAgICAgICAgICAgICAgICAibmFt
+ZSI6ICJ0ZW5hbnRfbmFtZTEiDQogICAgICAgICAgICB9DQogICAgICAgIH0sDQog
+ICAgICAgICJzZXJ2aWNlQ2F0YWxvZyI6IFsNCiAgICAgICAgICAgIHsNCiAgICAg
+ICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAgICAgICAgICAg
+ICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAg
+ICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6
+ODc3Ni92MS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIsDQogICAg
+ICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSIsDQogICAg
+ICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4w
+LjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwN
+CiAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0cDovLzEy
+Ny4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdh
+Ig0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAg
+ICAgICAgICAgICAgICAidHlwZSI6ICJ2b2x1bWUiLA0KICAgICAgICAgICAgICAg
+ICJuYW1lIjogInZvbHVtZSINCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7
+DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0KICAgICAg
+ICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAgIHsN
+CiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRwOi8vMTI3
+LjAuMC4xOjkyOTIvdjEiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lv
+biI6ICJyZWdpb25PbmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVy
+bmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQogICAgICAgICAg
+ICAgICAgICAgICAgICAicHVibGljVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5
+Mi92MSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgIF0s
+DQogICAgICAgICAgICAgICAgInR5cGUiOiAiaW1hZ2UiLA0KICAgICAgICAgICAg
+ICAgICJuYW1lIjogImdsYW5jZSINCiAgICAgICAgICAgIH0sDQogICAgICAgICAg
+ICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0KICAg
+ICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAg
+IHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRwOi8v
+MTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2
+NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lv
+bk9uZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAi
+aHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YxLjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBm
+Y2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInB1YmxpY1VS
+TCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVl
+OGE2MGZjZjg5YmI2NjE3YSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAg
+ICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgInR5cGUiOiAiY29tcHV0ZSIs
+DQogICAgICAgICAgICAgICAgIm5hbWUiOiAibm92YSINCiAgICAgICAgICAgIH0s
+DQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5r
+cyI6IFtdLA0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAg
+ICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVS
+TCI6ICJodHRwOi8vMTI3LjAuMC4xOjM1MzU3L3YyLjAiLA0KICAgICAgICAgICAg
+ICAgICAgICAgICAgInJlZ2lvbiI6ICJSZWdpb25PbmUiLA0KICAgICAgICAgICAg
+ICAgICAgICAgICAgImludGVybmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUz
+NTcvdjIuMCIsDQogICAgICAgICAgICAgICAgICAgICAgICAicHVibGljVVJMIjog
+Imh0dHA6Ly8xMjcuMC4wLjE6NTAwMC92Mi4wIg0KICAgICAgICAgICAgICAgICAg
+ICB9DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAidHlwZSI6
+ICJpZGVudGl0eSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAia2V5c3RvbmUi
+DQogICAgICAgICAgICB9DQogICAgICAgIF0sDQogICAgICAgICJ1c2VyIjogew0K
+ICAgICAgICAgICAgInVzZXJuYW1lIjogInJldm9rZWRfdXNlcm5hbWUxIiwNCiAg
+ICAgICAgICAgICJyb2xlc19saW5rcyI6IFsNCiAgICAgICAgICAgICAgICAicm9s
+ZTEiLA0KICAgICAgICAgICAgICAgICJyb2xlMiINCiAgICAgICAgICAgIF0sDQog
+ICAgICAgICAgICAiaWQiOiAicmV2b2tlZF91c2VyX2lkMSIsDQogICAgICAgICAg
+ICAicm9sZXMiOiBbDQogICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAg
+ICAgICAibmFtZSI6ICJyb2xlMSINCiAgICAgICAgICAgICAgICB9LA0KICAgICAg
+ICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAicm9sZTIi
+DQogICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgXSwNCiAgICAgICAgICAg
+ICJuYW1lIjogInJldm9rZWRfdXNlcm5hbWUxIg0KICAgICAgICB9DQogICAgfQ0K
+fQ0KMYIByjCCAcYCAQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh
+Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv
+cGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZAIBETAHBgUrDgMCGjAN
+BgkqhkiG9w0BAQEFAASCAQAxJMbNZf0/IWg/+/ciWQr9yuW9M48hQdaHcN+t6qvZ
+OlPev8N1tP8pNTupW9LXt0N8ZU/8AzPLPeRXHqd4lzuDV6ttesfLL3Ag410o4Elb
+Aum11Y1kDGlbwnaYoD9m07FML1ZfOWJ81Z0CITVGGRX90e+jlYjtnmdshmi2saVl
+r/Sae6ta52gjptaZE9tOu42uXlfhWNuC0/W7lRuWbWSHZENZWtTHHz2Q+v/HxORf
+jY3kwSaVEkx9faQ9Npy6J+rSQg+lIMRAYw/rFWedEsP9MzHKBcKTXid0yIQ2ox1r
+1Em3WapL1FDpwJtHaaL92WTEQulpxJUcmzPgEd5H78+Q
+-----END CMS-----
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_revoked.pkiz b/keystonemiddleware-moon/examples/pki/cms/auth_token_revoked.pkiz
new file mode 100644 (file)
index 0000000..9fbe8ea
--- /dev/null
@@ -0,0 +1 @@
+PKIZ_eJylVtly4jgUfddXzHuqK9jGED_Mgze8BInYeEF-8wJeBYTF29ePbEh3p9OZycxQRZUtS_eee87Rlb59oz9J1Qz0hwzXw8s3AA1DZxpsPh8CI6tjJFqxfKBjnSLL0pMli5bayo6oS6l7UlIoawUd31qavH7V1kbEAcVSdTGkg4mrpunG3nZmhllUxRzMV7k0N_b0eR8cMespeGNnkSbsjeKQ-tw5j8jiAoK1MTNkk43Ylol8N1_KYh74fBlrwjHa2_3bZOzbl9DnPbdsaGAxD3V7EiuHGix7tUPdtFkW4hU6hynqY3bJ4XbZ4wkuAgLZIMcsZGBv9ch3p9jBTUAQWSlVjgvMAugkmZE3qbE3q4Ct6igfEXWBnxwjln-JyA0VzT4JNuYV--07FGCA8X9QgAHGDxQSg0l7xIy3duQRySHR7WaVP9XQMbgxgTxtV0XKoR7XSaHWABV2jgjuA2IWuHd7pEAmcLIMFRLBLJ6ufDNHBW4Rq-Y7b3KmQSfbjVQN5Br7oAaR7l2oEsOHKiJ2E7HVNdHRLtKqa3iTMtps6EL9JttdtX2kLa6YdXPwb2X7hS8ewKLsBsL-qxLgs8jvA39OLnjPbtmtHGNg9yNhpLpgP6nGgMS7BrpUD4hAzAhn-nCKOxp5cUl26yal-4HCZO4L-Toh6qcWB18kazDXZDQX1f5n6cE_aT9kjom3D33hetP-TnQpXAf5Aa1zgFTFhM-ixVccaA0cXeH6iUWawYKgoGAIKpADJ7D3qpWmslALiqBIeUwMFhUqh29GaxLfpHyhL22m39b7u3LB33qdoDraSEyifWw0G7Y9RuTSg1EOhhGWMm1fAw-0K43wWI-PObt-c-FndgdfkLCn_DCoE1iYT5tfLT-osP5q9_ldcPAx-lebittARaxBUhh0wBQ262GxzcfanQPfrmi9x0QvPyVw4AIMBN4X15S40W10L1RbXTpSB46TjMJoYJ9eoKJeoJO5sFBn0LFmUElCcINNs5HFNRkg085Ds2W0jCoY3-0u8d1B3h8b7G3-QriCYRDenFYGG1TEpGoS7d5UNJ6JtGb4dgxufEyG4LSMXehbrbGf3PbC_WND-1wR-FkdaXRv5KYw1J5s6NGW35DFRDjTJO_6JaCa0gXuW0sbnjujmvwC2awSIpwC396NAW-GG9fcA3j9zwfmvfN29Lyk5ZkfXDoicYzR-kMJTMx63c8Lg00wKFJuOK-_Geo7T2_lfp8D7pPupDDCztFkMT40aaprYqpK0NBK-t9C69DIIlY8y1qojcpA69zIFlYAHdDUxvTcXl1CsdRExlVlCcrWRG3VQrSkFHmSGDuyh5iI8HxCFhS-uoaSOM4FcgZNh5OqqEIT7KMTtNVGacZMS7XJlsGm6hONti9HraAMv99M6MXEFG3sgx_b1hOjIdD-FmhJhC7oVRdKxphJbOHSZb1zkEtO6CfXwKfXH5oMSA1ePDdTRcwOjWL9fFdSJckS6bVHFfF1IvDP-CWbCmXy9NpVu_BpqcRivc16oLGr4hK_vmoz1BDkvSxetosqVk-l6J5X-elhpsFty70GHNfuNX6VQnbGwedWP0pnp9wFMTBTn1wV_hryDJ7He69j2piEh31eh4yyeDTnVnOUqwekOJskWmXPiGm6R-UlY4xz-ZjMe0C6bus-TBfLy45cLuHM19gyW1Df1s5JbjUu1XU3FphSW7XS6UnvrDYL42XW7YvwyD-fOhBCxpuHZbEsrSeTeY6cR3W5TY66RQ4MmmvZUYXRflFI5uuWEecPjMA9If-BMIFQZVOb04E_O0ai7my7iTy3iyjLPXa6O678kDwyBSTepGIrln2AO_U4mzlzS-TU7WP1_DJr_vwTjHdVFSk_7q1_AfJ_mjc=
\ No newline at end of file
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped.json b/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped.json
new file mode 100644 (file)
index 0000000..698e01d
--- /dev/null
@@ -0,0 +1,85 @@
+{
+    "access": {
+        "token": {
+            "expires": "2038-01-18T21:14:07Z",
+            "id": "placeholder",
+            "tenant": {
+                "id": "tenant_id1",
+                "enabled": true,
+                "description": null,
+                "name": "tenant_name1"
+            }
+        },
+        "serviceCatalog": [
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a"
+                    }
+                ],
+                "type": "volume",
+                "name": "volume"
+            },
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:9292/v1",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:9292/v1",
+                        "publicURL": "http://127.0.0.1:9292/v1"
+                    }
+                ],
+                "type": "image",
+                "name": "glance"
+            },
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a"
+                    }
+                ],
+                "type": "compute",
+                "name": "nova"
+            },
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:35357/v2.0",
+                        "region": "RegionOne",
+                        "internalURL": "http://127.0.0.1:35357/v2.0",
+                        "publicURL": "http://127.0.0.1:5000/v2.0"
+                    }
+                ],
+                "type": "identity",
+                "name": "keystone"
+            }
+        ],
+        "user": {
+            "username": "user_name1",
+            "roles_links": [
+                "role1",
+                "role2"
+            ],
+            "id": "user_id1",
+            "roles": [
+                {
+                    "name": "role1"
+                },
+                {
+                    "name": "role2"
+                }
+            ],
+            "name": "user_name1"
+        }
+    }
+}
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped.pem b/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped.pem
new file mode 100644 (file)
index 0000000..4a5b3a2
--- /dev/null
@@ -0,0 +1,75 @@
+-----BEGIN CMS-----
+MIINhwYJKoZIhvcNAQcCoIINeDCCDXQCAQExCTAHBgUrDgMCGjCCC5QGCSqGSIb3
+DQEHAaCCC4UEgguBew0KICAgICJhY2Nlc3MiOiB7DQogICAgICAgICJ0b2tlbiI6
+IHsNCiAgICAgICAgICAgICJleHBpcmVzIjogIjIwMzgtMDEtMThUMjE6MTQ6MDda
+IiwNCiAgICAgICAgICAgICJpZCI6ICJwbGFjZWhvbGRlciIsDQogICAgICAgICAg
+ICAidGVuYW50Ijogew0KICAgICAgICAgICAgICAgICJpZCI6ICJ0ZW5hbnRfaWQx
+IiwNCiAgICAgICAgICAgICAgICAiZW5hYmxlZCI6IHRydWUsDQogICAgICAgICAg
+ICAgICAgImRlc2NyaXB0aW9uIjogbnVsbCwNCiAgICAgICAgICAgICAgICAibmFt
+ZSI6ICJ0ZW5hbnRfbmFtZTEiDQogICAgICAgICAgICB9DQogICAgICAgIH0sDQog
+ICAgICAgICJzZXJ2aWNlQ2F0YWxvZyI6IFsNCiAgICAgICAgICAgIHsNCiAgICAg
+ICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAgICAgICAgICAg
+ICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAg
+ICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6
+ODc3Ni92MS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIsDQogICAg
+ICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSIsDQogICAg
+ICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4w
+LjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwN
+CiAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0cDovLzEy
+Ny4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdh
+Ig0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAg
+ICAgICAgICAgICAgICAidHlwZSI6ICJ2b2x1bWUiLA0KICAgICAgICAgICAgICAg
+ICJuYW1lIjogInZvbHVtZSINCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7
+DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0KICAgICAg
+ICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAgIHsN
+CiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRwOi8vMTI3
+LjAuMC4xOjkyOTIvdjEiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lv
+biI6ICJyZWdpb25PbmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVy
+bmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQogICAgICAgICAg
+ICAgICAgICAgICAgICAicHVibGljVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5
+Mi92MSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgIF0s
+DQogICAgICAgICAgICAgICAgInR5cGUiOiAiaW1hZ2UiLA0KICAgICAgICAgICAg
+ICAgICJuYW1lIjogImdsYW5jZSINCiAgICAgICAgICAgIH0sDQogICAgICAgICAg
+ICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0KICAg
+ICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAg
+IHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRwOi8v
+MTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2
+NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lv
+bk9uZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAi
+aHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YxLjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBm
+Y2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInB1YmxpY1VS
+TCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVl
+OGE2MGZjZjg5YmI2NjE3YSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAg
+ICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgInR5cGUiOiAiY29tcHV0ZSIs
+DQogICAgICAgICAgICAgICAgIm5hbWUiOiAibm92YSINCiAgICAgICAgICAgIH0s
+DQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5r
+cyI6IFtdLA0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAg
+ICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVS
+TCI6ICJodHRwOi8vMTI3LjAuMC4xOjM1MzU3L3YyLjAiLA0KICAgICAgICAgICAg
+ICAgICAgICAgICAgInJlZ2lvbiI6ICJSZWdpb25PbmUiLA0KICAgICAgICAgICAg
+ICAgICAgICAgICAgImludGVybmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUz
+NTcvdjIuMCIsDQogICAgICAgICAgICAgICAgICAgICAgICAicHVibGljVVJMIjog
+Imh0dHA6Ly8xMjcuMC4wLjE6NTAwMC92Mi4wIg0KICAgICAgICAgICAgICAgICAg
+ICB9DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAidHlwZSI6
+ICJpZGVudGl0eSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAia2V5c3RvbmUi
+DQogICAgICAgICAgICB9DQogICAgICAgIF0sDQogICAgICAgICJ1c2VyIjogew0K
+ICAgICAgICAgICAgInVzZXJuYW1lIjogInVzZXJfbmFtZTEiLA0KICAgICAgICAg
+ICAgInJvbGVzX2xpbmtzIjogWw0KICAgICAgICAgICAgICAgICJyb2xlMSIsDQog
+ICAgICAgICAgICAgICAgInJvbGUyIg0KICAgICAgICAgICAgXSwNCiAgICAgICAg
+ICAgICJpZCI6ICJ1c2VyX2lkMSIsDQogICAgICAgICAgICAicm9sZXMiOiBbDQog
+ICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJy
+b2xlMSINCiAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgIHsNCiAg
+ICAgICAgICAgICAgICAgICAgIm5hbWUiOiAicm9sZTIiDQogICAgICAgICAgICAg
+ICAgfQ0KICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICJuYW1lIjogInVzZXJf
+bmFtZTEiDQogICAgICAgIH0NCiAgICB9DQp9DQoxggHKMIIBxgIBATCBpDCBnjEK
+MAgGA1UEBRMBNTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlT
+dW5ueXZhbGUxEjAQBgNVBAoTCU9wZW5TdGFjazERMA8GA1UECxMIS2V5c3RvbmUx
+JTAjBgkqhkiG9w0BCQEWFmtleXN0b25lQG9wZW5zdGFjay5vcmcxFDASBgNVBAMT
+C1NlbGYgU2lnbmVkAgERMAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIIBAGFaC8Po
+svBez6wHfGxgqtX+Zk7kFH0xu/JA7fWp8L5e1k1q+wsSII/P6rATOXR8BSPwifat
+mKRan9kzerLeb3A5g07VphvHfVkDEVaeihi33bpt7140ELSKu/ogWQPtasjBM9Eb
+M9pS4N5NCtZ0erE5DgX//IRfrHFdZuhIbwlmei72692PV7Q70t/rbaH8ofIrH7Rz
+Z1Kuvj0+7tELgd52wy5YnU0e879OEj+2qUk30TvqRG9jdKxLSanmR/8dSA2eNNgO
+oHrtXc4EmpWFbP6yVxNwK3dQ6OvU4virV1YW5+De2ApLt+IeojaVPGnDPfsRvY5x
+t0eIwpDqkgvkRP8=
+-----END CMS-----
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped.pkiz b/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped.pkiz
new file mode 100644 (file)
index 0000000..34d7706
--- /dev/null
@@ -0,0 +1 @@
+PKIZ_eJylVst2ozgU3OsrZp_Tx4CNY5biaRFLGMx7ZyDGYMBObJ5fPwInpyedzkxmhhUIqVS36t4r_fhBH1HREPlDwrvx4wfACK1bM9CfziE6NjGBZiyd6dg1lyRxuZCgqXSSDddi6rzKKZa0cTxeaNLuRduhaA5kU1nDPR2MVkqaeo_PvX4MOFLEc5wZmfiIKvpehZeAc-XAt46RJlQoP6fe_JpFpXoD4Q4tkaRzEdexkedkGwlmocefYk24RJU1vE8OPOu293jXObUUGGb7tcXE8rkBm0HpSb9oNzmssX1ekCHmNvOg2wwBE-RhibkwCzjM4sEciOcsAjtow5KUhlxkQR5wANvJEWVtiiq9CLmiibKJUR96ySXi-G1U3lnR3ZnQ1-vA6z6wACON_8MCjDR-shDZoOwuAevubGlick7WVmtkqwbbaD5tIC06I0_nZAiaJFcaQHIrI2UwhKWeB4MzEBmzoX08klwsAy5YGJ6ekTzoCKdkB5e5UlDm2ReLUVxUhQ2I1u6NOjH-KKLSaiOuqJM1OURaUe_vVka-Txeu77a9uVZFmloHnJOBf2vbL3rxAOenfhTsvzoBvkL-CPy1uOCjuqfesNGo7mfByuIWeEkxAZZuHa7FZmQEYla40pfXuKfI6i057NqU1gOlyb4t5JukVL5McfBNscbkYqbkot7_1XrwT96PO8elW-09ob57_yb0SahH-wGNc6RUxCV_jNTvZKA5alTj3YojGuJAmFMyJcmJjRk8uIWhKRzWwjzMUz4oEUdyZR7cE61NPJ3qRb5VTL-N93fhgr_N9ZI0kS-yifa50fhcd4nK2wAmO1hW2Ei0fY060K400eNcPp5bzXsWfpXu4BsWDlQflvQCh7NF-2vKjy7svtt9fgcOPqN_t6k4LZZhA5Ic0QFd8HfjYouPtTcNPKug8V6S9elLAUctwCjg2-KGCjdlG62F4nktXmgGTpNQjlo8pDcsKzdsHx2cK0tsm0ssJ3twp013K9U6GSnTzkN3O9IwinD6tvrEc0Z7fxbY-3xVqME4iO-Zdgp9ksdl0SbaW1PReDbS2vHfJbzrwYzgNIzD3jM7VDH3Wnj72dI-l4fesYk0WhuZLoyxJz492rI7s7gUrnSTD_0SUE_pAue9pY3vPSqYXyi7A7X1MDVV-71CRzCcgRHlQwN5B6w-deKenp8Fzt4dm0DvGny1C41zsnQKoxAuoUzrxWcFHCCxp8c8jAMJ0PO_Tfdmm4aLTsohElPiitCxoe100gD1-3dgw8K1sXltJTOQXdNESqvLpq3sABahBllHETusO3O3jqqCoylcYAu1CpwmPyltsY01t3bmFr07XDvFhts78NUGknIrnn3C0Fqgdjotav96WzmJ6jF8Df1iSDTawhyxGYHiO1AdzfUKYMtslXTaSVbamx16XYlUcgkpYEgjUj5cbyAR09PL8ZRpQsuINHwVQLij9yBp74o5-3C9beMjRm4RGubu5K2F9HGJocPh_HJ7OM-zk36Nb-eHw2sxnGZ74rvrAqi2wSpx1jJyNWd7CHM1LftoqJiSh-nGUy32Js_OzhI1jmuXPJJmF9hh5aytDpquHbdgGGbIvIVPr71BcFdDy7fk2ZFJ92m33szIIMlu-IIEf-UzJFJOwolZRZ1hz-ONETD7_AwstzFmO7fpltxy63KH5wd0qXbBIt7HrOs-YWgF-_PT7CF9KnouPykraZg9YN1WOdW_7O0ckPm5UMNs268OL8QpD24qFNvu8eHFEjtI2uct79Qmn3P8cWWacap2kXw1ZCHP4Gzj16QE2-r1YrVQqwweOk_ybmMdDF83-GVNIJjuogqRf95L_wRcTpJ3
\ No newline at end of file
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped_expired.json b/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped_expired.json
new file mode 100644 (file)
index 0000000..04ec9f3
--- /dev/null
@@ -0,0 +1,85 @@
+{
+    "access": {
+        "token": {
+            "expires": "2010-06-02T14:47:34Z",
+            "id": "placeholder",
+            "tenant": {
+                "id": "tenant_id1",
+                "enabled": true,
+                "description": null,
+                "name": "tenant_name1"
+            }
+        },
+        "serviceCatalog": [
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a"
+                    }
+                ],
+                "type": "volume",
+                "name": "volume"
+            },
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:9292/v1",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:9292/v1",
+                        "publicURL": "http://127.0.0.1:9292/v1"
+                    }
+                ],
+                "type": "image",
+                "name": "glance"
+            },
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a"
+                    }
+                ],
+                "type": "compute",
+                "name": "nova"
+            },
+            {
+                "endpoints_links": [],
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:35357/v2.0",
+                        "region": "RegionOne",
+                        "internalURL": "http://127.0.0.1:35357/v2.0",
+                        "publicURL": "http://127.0.0.1:5000/v2.0"
+                    }
+                ],
+                "type": "identity",
+                "name": "keystone"
+            }
+        ],
+        "user": {
+            "username": "user_name1",
+            "roles_links": [
+                "role1",
+                "role2"
+            ],
+            "id": "user_id1",
+            "roles": [
+                {
+                    "name": "role1"
+                },
+                {
+                    "name": "role2"
+                }
+            ],
+            "name": "user_name1"
+        }
+    }
+}
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped_expired.pem b/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped_expired.pem
new file mode 100644 (file)
index 0000000..c3de8bb
--- /dev/null
@@ -0,0 +1,75 @@
+-----BEGIN CMS-----
+MIINhwYJKoZIhvcNAQcCoIINeDCCDXQCAQExCTAHBgUrDgMCGjCCC5QGCSqGSIb3
+DQEHAaCCC4UEgguBew0KICAgICJhY2Nlc3MiOiB7DQogICAgICAgICJ0b2tlbiI6
+IHsNCiAgICAgICAgICAgICJleHBpcmVzIjogIjIwMTAtMDYtMDJUMTQ6NDc6MzRa
+IiwNCiAgICAgICAgICAgICJpZCI6ICJwbGFjZWhvbGRlciIsDQogICAgICAgICAg
+ICAidGVuYW50Ijogew0KICAgICAgICAgICAgICAgICJpZCI6ICJ0ZW5hbnRfaWQx
+IiwNCiAgICAgICAgICAgICAgICAiZW5hYmxlZCI6IHRydWUsDQogICAgICAgICAg
+ICAgICAgImRlc2NyaXB0aW9uIjogbnVsbCwNCiAgICAgICAgICAgICAgICAibmFt
+ZSI6ICJ0ZW5hbnRfbmFtZTEiDQogICAgICAgICAgICB9DQogICAgICAgIH0sDQog
+ICAgICAgICJzZXJ2aWNlQ2F0YWxvZyI6IFsNCiAgICAgICAgICAgIHsNCiAgICAg
+ICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAgICAgICAgICAg
+ICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAg
+ICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6
+ODc3Ni92MS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIsDQogICAg
+ICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSIsDQogICAg
+ICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4w
+LjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwN
+CiAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0cDovLzEy
+Ny4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdh
+Ig0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAg
+ICAgICAgICAgICAgICAidHlwZSI6ICJ2b2x1bWUiLA0KICAgICAgICAgICAgICAg
+ICJuYW1lIjogInZvbHVtZSINCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7
+DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0KICAgICAg
+ICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAgIHsN
+CiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRwOi8vMTI3
+LjAuMC4xOjkyOTIvdjEiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lv
+biI6ICJyZWdpb25PbmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVy
+bmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQogICAgICAgICAg
+ICAgICAgICAgICAgICAicHVibGljVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5
+Mi92MSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgIF0s
+DQogICAgICAgICAgICAgICAgInR5cGUiOiAiaW1hZ2UiLA0KICAgICAgICAgICAg
+ICAgICJuYW1lIjogImdsYW5jZSINCiAgICAgICAgICAgIH0sDQogICAgICAgICAg
+ICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0KICAg
+ICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAg
+IHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRwOi8v
+MTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2
+NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lv
+bk9uZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAi
+aHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YxLjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBm
+Y2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInB1YmxpY1VS
+TCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVl
+OGE2MGZjZjg5YmI2NjE3YSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAg
+ICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgInR5cGUiOiAiY29tcHV0ZSIs
+DQogICAgICAgICAgICAgICAgIm5hbWUiOiAibm92YSINCiAgICAgICAgICAgIH0s
+DQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5r
+cyI6IFtdLA0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAg
+ICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVS
+TCI6ICJodHRwOi8vMTI3LjAuMC4xOjM1MzU3L3YyLjAiLA0KICAgICAgICAgICAg
+ICAgICAgICAgICAgInJlZ2lvbiI6ICJSZWdpb25PbmUiLA0KICAgICAgICAgICAg
+ICAgICAgICAgICAgImludGVybmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUz
+NTcvdjIuMCIsDQogICAgICAgICAgICAgICAgICAgICAgICAicHVibGljVVJMIjog
+Imh0dHA6Ly8xMjcuMC4wLjE6NTAwMC92Mi4wIg0KICAgICAgICAgICAgICAgICAg
+ICB9DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAidHlwZSI6
+ICJpZGVudGl0eSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAia2V5c3RvbmUi
+DQogICAgICAgICAgICB9DQogICAgICAgIF0sDQogICAgICAgICJ1c2VyIjogew0K
+ICAgICAgICAgICAgInVzZXJuYW1lIjogInVzZXJfbmFtZTEiLA0KICAgICAgICAg
+ICAgInJvbGVzX2xpbmtzIjogWw0KICAgICAgICAgICAgICAgICJyb2xlMSIsDQog
+ICAgICAgICAgICAgICAgInJvbGUyIg0KICAgICAgICAgICAgXSwNCiAgICAgICAg
+ICAgICJpZCI6ICJ1c2VyX2lkMSIsDQogICAgICAgICAgICAicm9sZXMiOiBbDQog
+ICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJy
+b2xlMSINCiAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgIHsNCiAg
+ICAgICAgICAgICAgICAgICAgIm5hbWUiOiAicm9sZTIiDQogICAgICAgICAgICAg
+ICAgfQ0KICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICJuYW1lIjogInVzZXJf
+bmFtZTEiDQogICAgICAgIH0NCiAgICB9DQp9DQoxggHKMIIBxgIBATCBpDCBnjEK
+MAgGA1UEBRMBNTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlT
+dW5ueXZhbGUxEjAQBgNVBAoTCU9wZW5TdGFjazERMA8GA1UECxMIS2V5c3RvbmUx
+JTAjBgkqhkiG9w0BCQEWFmtleXN0b25lQG9wZW5zdGFjay5vcmcxFDASBgNVBAMT
+C1NlbGYgU2lnbmVkAgERMAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIIBALYxBjRE
+hecjo98fUdki3cwcpGU8zY8XHQa4x15WGkPxkI1HwSYaId/WjrOWP2CxmT3vVe7Z
+lqV2a0YmdPx9zdDm09VmoiZr3HxYaNzXztT817dECYINCgz33EnansIyPHG2hjOR
+4Gt7R26MXf+AIRiCNuCFZPnHI1pfCbwuky9/iBokvE9mThA+bVrUPZd/2+jp4s3B
+n3+fbC+FCoZ5t522wGgEtVyMNvC90Wvvuf2mx7baXNo4/0ZG8C86lT+qmMe22zlf
++DxmJl149p419zdv6rzTU7p2OeTBnkdw1GsEqKyvtHYxzAjLYjiJo6jyaERXBaLm
+/J7ZRSBmhHoLuWk=
+-----END CMS-----
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped_expired.pkiz b/keystonemiddleware-moon/examples/pki/cms/auth_token_scoped_expired.pkiz
new file mode 100644 (file)
index 0000000..766b4cd
--- /dev/null
@@ -0,0 +1 @@
+PKIZ_eJylVtlyozgUfddXzHuqK2xOzCObMdiSzW7pzUCMwchLbNavH4GT6kmnM5OZcZWrQEhH555z75V-_GA_1TAt9IcGveHlB4CWNW8cbC9OxNrXCVKcRDuxsWuhaeqTpCmO0Wq-Mlez4FXPoGYO44lkat7F9KxYBLpjzJUtG4ynRpZFzy-dvccCKhMR5qtcfbaO7PlIzlgIdbxx97EpH63ilEXiNY_p7AaIZz1Zmi3EQsvHUZAvNSUn0eSQmPI5Prr9-2QcubdtNAmDQ8OAlXw7d7lEP9Vg2Rsd6qRmWSgV9E8S6hNhKeJ22WMOF4RCgeRYgDzsnR5FgYR93BCK6Eovc1xgAUA_3Vt5k1lHuyRCWcf5yKgjUXqOhck6pndWbHeObOwKR-0HFmCg8X9YgIHGTxYqj2l7xnzo-drI5JTO3WaVT2voW-K4gSa1qyITUY_rtDBqgAo3RxT3hNoF7oMe6ZAn_n6PCpViAUuryM5RgVskGPku5K4MlHvZqOUgrnUkNYjn4Y05MXwoY-o2sVBW6RztYrOstncr482GLZzfbXtz7RibswoLQQ7-rW2_6DUBsDh0g2D_1QnwFfJH4K_FBR_VPXQr3xrU_SwYLW84SssRkIYVmav1wAgkvHxlD69Jx5Bnt3TnNRmrB0aTf1s4qVNqfJni4JtiDcnFjcnFvP-r9eCfvB92Tmh43EZydff-TeiDXA32AxbnQKlM6GQfz76Tgc6gUQW9qYBMSwCkYGQoKpAPOdiH5co0BGiSghTZBFNLQIUh4nuiNWlkM73Qt4rpt_H-Llzwt7lOUR1vVD41PzeajdCeY3rrwWgHz8tLjbWvQQfWlUZ6QjhJRLd-z8Kv0h18w8Ke6cOjThZgLjW_pvzggvfd7vM7cPAZ_btNJWigrtQgLSw2YMsbb1jsThLzTYPILVm853R--FLAQQswCPi2uGbCjdnGaqF8matnloHjJKuwGugrN6hj9rcD6DtPSE-eYO9uwZ02243OqnSgzDoP223PwijJ-O52aRQM9v4ssPf5M7kCwyC8Z9qBbFCR0LJJzbemYk742GyGb2dy14MbwFkYu23ktNaRu9fC28eG9bmCRPs6Nllt5LY8xJ5u2NGW35klVL6yTT70S8A8ZQuC95Y2PHdWyf1COeyZrbuxqfrvFTqAwRwMKB8ayDvg8VMn7tj5WcL83bER9K7BV7uwOEdLxzBK-Ux0Vi8bXobYUjt2zCsJ1gA7_5ts6zQZkVqtUCw1Q6GqBL7iB63WK_b9HftKGfrQuTaag_XQcSyjsXXHNzwAVcVU-MBQW2gHYljFx1JgKVxC12oMZZy8MJpynZhhFYguuztcW8NX1nfgqw8041a-bBDHaoHZGTRW89fbykGd7ckr2ZR9arIWFqj1AJTcgapYtI8Auk5jZONOutHcfBK11JqhM2GAhEVkfLjeKEjNDpf9ITflhlNZ-DOgKB67B2niTXTXpH1IYeWIT09VZWNhm5pu_7LFotenk40hKN5tMWmeLuGz5F_p9Lw8CZct2Exj5Vhc1ig3oPTgy6G0cGOnnYclRPPLjp6a5elZauAxWJk7U3pep74japd2cbW6ykoJIP5aWuX7hwdztjNlszcnrfuwmnC8LJSzZ11Osktpha621jm0Jdw6epycXy3yWK5odqWiC66rXBCk-CJeBffxOaJazV2mNJhOt4l2eFXI3o0Wt2oBV3SWRiePSlr56B_UY9dRTz2YEvCb9bK-zFdQrRHO5cuZqx5fIiHT1CZ3-SQq7Cpz7MNRvjxORbSpQnmy7B7YRZI_16hsr-B6Pb2IF9vVHjxzkSbJLjhEi9h4DOIVBeNd1ED6z3vpnxbOkgI=
\ No newline at end of file
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_unscoped.json b/keystonemiddleware-moon/examples/pki/cms/auth_token_unscoped.json
new file mode 100644 (file)
index 0000000..4156688
--- /dev/null
@@ -0,0 +1,23 @@
+{
+    "access": {
+        "token": {
+            "expires": "2112-08-17T15:35:34Z",
+            "id": "01e032c996ef4406b144335915a41e79"
+        },
+        "serviceCatalog": {},
+        "user": {
+            "username": "user_name1",
+            "roles_links": [],
+            "id": "c9c89e3be3ee453fbf00c7966f6d3fbd",
+            "roles": [
+                {
+                    "name": "role1"
+                },
+                {
+                    "name": "role2"
+                }
+            ],
+            "name": "user_name1"
+        }
+    }
+}
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_unscoped.pem b/keystonemiddleware-moon/examples/pki/cms/auth_token_unscoped.pem
new file mode 100644 (file)
index 0000000..6855221
--- /dev/null
@@ -0,0 +1,25 @@
+-----BEGIN CMS-----
+MIIERgYJKoZIhvcNAQcCoIIENzCCBDMCAQExCTAHBgUrDgMCGjCCAlMGCSqGSIb3
+DQEHAaCCAkQEggJAew0KICAgICJhY2Nlc3MiOiB7DQogICAgICAgICJ0b2tlbiI6
+IHsNCiAgICAgICAgICAgICJleHBpcmVzIjogIjIxMTItMDgtMTdUMTU6MzU6MzRa
+IiwNCiAgICAgICAgICAgICJpZCI6ICIwMWUwMzJjOTk2ZWY0NDA2YjE0NDMzNTkx
+NWE0MWU3OSINCiAgICAgICAgfSwNCiAgICAgICAgInNlcnZpY2VDYXRhbG9nIjog
+e30sDQogICAgICAgICJ1c2VyIjogew0KICAgICAgICAgICAgInVzZXJuYW1lIjog
+InVzZXJfbmFtZTEiLA0KICAgICAgICAgICAgInJvbGVzX2xpbmtzIjogW10sDQog
+ICAgICAgICAgICAiaWQiOiAiYzljODllM2JlM2VlNDUzZmJmMDBjNzk2NmY2ZDNm
+YmQiLA0KICAgICAgICAgICAgInJvbGVzIjogWw0KICAgICAgICAgICAgICAgIHsN
+CiAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAicm9sZTEiDQogICAgICAgICAg
+ICAgICAgfSwNCiAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAg
+ICJuYW1lIjogInJvbGUyIg0KICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAg
+IF0sDQogICAgICAgICAgICAibmFtZSI6ICJ1c2VyX25hbWUxIg0KICAgICAgICB9
+DQogICAgfQ0KfQ0KMYIByjCCAcYCAQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK
+EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr
+ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZAIBETAH
+BgUrDgMCGjANBgkqhkiG9w0BAQEFAASCAQAXNWXYv3q2EcEjigKDJEOvnKBGTHeV
+o9iwYmtdJ2kKtbuZiSGOcWymxNtv//IPMmNDWZ/uwDZt37YdPwCMRJa79h6dastD
+5slEZGMxgFekm/1yqpV2F7xGqGIED2rNTeBlVnYS6ZOL8hCqekPb1OqXZ3vDaHtQ
+rrBzNP8RbWS4MyUoVZtSEYANjJVp/zou/pYASml9iNPPKrl2xRgYuzaAirVIiTZt
+QZY4LQYnHdVBLTZ0fQQugohTba789ix0U79ReQrIOqnBD3OnmN0uRovu5s1HYyre
+c67FixOpNgA4IBFsqYG2feP6ZF1zCmAaRYX4LpprZLGzg/aPHxqjXGsT
+-----END CMS-----
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_token_unscoped.pkiz b/keystonemiddleware-moon/examples/pki/cms/auth_token_unscoped.pkiz
new file mode 100644 (file)
index 0000000..13c5e40
--- /dev/null
@@ -0,0 +1 @@
+PKIZ_eJx9VMmSozgQvfMVfa-oMAbbVRzmIAlZCFvQGLHewAs72MaY5esHuzt65tSKUEiZkS_z5RL5-TkfiAk1fiBmv4RPgVGq7kCg75qQps-jAawjamYd4QiBwUHAwgPiQIOJc1cThkg-67lDkH0jNo1lQbWwBqJZaQc4SXB2HvU0kIzyKLPMzOAXred_HV4DyVUD_5DGRKlp3iRnWWwp0kUhlh5lnNEN1dos9NM-8vXyOM4yoiPjeNxzsNpzLLsqXpo5e13Ry-gLfA0R3QizYc88p2eTnpu8kEIvEA0VSEGO55dNBi8Gw8PibCObtq7sEchO_szqd1DhWClt6BuXmJRd9It27Nt9Qqt1GnvOLP8GlEoXeMuS2e_oYywNb6YC3T6-_m_8dshxdpmdzPV4g14501p_xsQZab08_WEx44S_RHnnOL-56bGV6TlTUDlT6DmiwY0qqIKeESYLJg-kMA8LJoVZiHTl4otDkmi7ub1wSCgEHMGrimCd4x0DCQFLB8MDgwbHewYKIrwVKUOuywY0AR0mhgtBwkFhQHagPQaB6lqWhvuSn7x1d_bDuZXOgHNgvWwFCBqOHKUPvTU_kW0eTfjAwPc7EhoYtSV3fZQPz7hyBp2DHCbFLS0yovQiRBb2hG31KM--IcbSurTI29H0djSun8fqOGxVYP9ixThaGmVMgsSRyjqu3AIk-CAwcCTQbk3Q04gB8c-IzhMKgeUAONcCbO8atS73i3mAGF0iWEaZWKcHN11FAj1_r8a1F5ZGKDWGyD468ZlOstqwRb1jnp5-5fK-M-cJvXSTbE6Vxqs4Sg9dUQdNcSuE_Cfc3JzH-fqxLruP-wpoqpNGV9iP8lMuzsmGtUkY1PCeUyJHQ7Nl2vfJslSkKOoJWpOw21fD1JDztsjbyx27Hw95icVWut-JOC6a_SUK-k1AmpUrNtpjm3T5osNNEn608g1lsSOgZBVvppgUhx2vm-5ate56rZynjSgam_tr6J7awn9y4n5Lth48bJRdy6Wx8m52ju7IE1Z-G92-ldZegIXrbm6gHJuBT63Ss1g3be9i5-ZTVotYxMm5WNrPXaB2_PpzsPt_hPdKwYb633r5FzKfcIU=
\ No newline at end of file
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_revoked.json b/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_revoked.json
new file mode 100644 (file)
index 0000000..c5dc01a
--- /dev/null
@@ -0,0 +1,88 @@
+{
+    "token": {
+        "catalog": [
+            {
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a"
+                    }
+                ],
+                "endpoints_links": [],
+                "type": "volume",
+                "name": "volume"
+            },
+            {
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:9292/v1",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:9292/v1",
+                        "publicURL": "http://127.0.0.1:9292/v1"
+                    }
+                ],
+                "endpoints_links": [],
+                "type": "image",
+                "name": "glance"
+            },
+            {
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne",
+                        "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a"
+                    }
+                ],
+                "endpoints_links": [],
+                "type": "compute",
+                "name": "nova"
+            },
+            {
+                "endpoints": [
+                    {
+                        "adminURL": "http://127.0.0.1:35357/v3",
+                        "region": "RegionOne",
+                        "internalURL": "http://127.0.0.1:35357/v3",
+                        "publicURL": "http://127.0.0.1:5000/v3"
+                    }
+                ],
+                "endpoints_links": [],
+                "type": "identity",
+                "name": "keystone"
+            }
+        ],
+        "expires_at": "2038-01-18T21:14:07Z",
+        "project": {
+            "enabled": true,
+            "description": null,
+            "name": "tenant_name1",
+            "id": "tenant_id1",
+            "domain": {
+                "id": "domain_id1",
+                "name": "domain_name1"
+            }
+        },
+        "user": {
+            "name": "revoked_username1",
+            "id": "revoked_user_id1",
+            "domain": {
+                "id": "domain_id1",
+                "name": "domain_name1"
+            }
+        },
+        "roles": [
+            {
+                "name": "role1"
+            },
+            {
+                "name": "role2"
+            }
+        ],
+        "methods": [
+            "password"
+        ]
+    }
+}
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_revoked.pem b/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_revoked.pem
new file mode 100644 (file)
index 0000000..94a077b
--- /dev/null
@@ -0,0 +1,76 @@
+-----BEGIN CMS-----
+MIINrQYJKoZIhvcNAQcCoIINnjCCDZoCAQExCTAHBgUrDgMCGjCCC7oGCSqGSIb3
+DQEHAaCCC6sEggunew0KICAgICJ0b2tlbiI6IHsNCiAgICAgICAgImNhdGFsb2ci
+OiBbDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50cyI6
+IFsNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAg
+ICAgImFkbWluVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3Ni92MS82NGI2ZjNm
+YmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAg
+ICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSIsDQogICAgICAgICAgICAgICAgICAg
+ICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2L3YxLzY0
+YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwNCiAgICAgICAgICAgICAg
+ICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2L3Yx
+LzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIg0KICAgICAgICAgICAg
+ICAgICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAi
+ZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAgICAgICAgICAgICAgInR5cGUiOiAi
+dm9sdW1lIiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJ2b2x1bWUiDQogICAg
+ICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICJlbmRw
+b2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAg
+ICAgICAgICAgICJhZG1pblVSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjkyOTIvdjEi
+LA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6ICJyZWdpb25PbmUi
+LA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVybmFsVVJMIjogImh0dHA6
+Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQogICAgICAgICAgICAgICAgICAgICAgICAi
+cHVibGljVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSINCiAgICAgICAg
+ICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAg
+ICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0KICAgICAgICAgICAgICAgICJ0eXBl
+IjogImltYWdlIiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJnbGFuY2UiDQog
+ICAgICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICJl
+bmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAg
+ICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQv
+djEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIsDQogICAgICAg
+ICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSIsDQogICAgICAg
+ICAgICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAu
+MTo4Nzc0L3YxLjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYxN2EiLA0K
+ICAgICAgICAgICAgICAgICAgICAgICAgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3
+LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3
+YSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgIF0sDQog
+ICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0KICAgICAgICAg
+ICAgICAgICJ0eXBlIjogImNvbXB1dGUiLA0KICAgICAgICAgICAgICAgICJuYW1l
+IjogIm5vdmEiDQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAg
+ICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAgIHsN
+CiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRwOi8vMTI3
+LjAuMC4xOjM1MzU3L3YzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdp
+b24iOiAiUmVnaW9uT25lIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRl
+cm5hbFVSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjM1MzU3L3YzIiwNCiAgICAgICAg
+ICAgICAgICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo1
+MDAwL3YzIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAg
+XSwNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAg
+ICAgICAgICAgICAgInR5cGUiOiAiaWRlbnRpdHkiLA0KICAgICAgICAgICAgICAg
+ICJuYW1lIjogImtleXN0b25lIg0KICAgICAgICAgICAgfQ0KICAgICAgICBdLA0K
+ICAgICAgICAiZXhwaXJlc19hdCI6ICIyMDM4LTAxLTE4VDIxOjE0OjA3WiIsDQog
+ICAgICAgICJwcm9qZWN0Ijogew0KICAgICAgICAgICAgImVuYWJsZWQiOiB0cnVl
+LA0KICAgICAgICAgICAgImRlc2NyaXB0aW9uIjogbnVsbCwNCiAgICAgICAgICAg
+ICJuYW1lIjogInRlbmFudF9uYW1lMSIsDQogICAgICAgICAgICAiaWQiOiAidGVu
+YW50X2lkMSIsDQogICAgICAgICAgICAiZG9tYWluIjogew0KICAgICAgICAgICAg
+ICAgICJpZCI6ICJkb21haW5faWQxIiwNCiAgICAgICAgICAgICAgICAibmFtZSI6
+ICJkb21haW5fbmFtZTEiDQogICAgICAgICAgICB9DQogICAgICAgIH0sDQogICAg
+ICAgICJ1c2VyIjogew0KICAgICAgICAgICAgIm5hbWUiOiAicmV2b2tlZF91c2Vy
+bmFtZTEiLA0KICAgICAgICAgICAgImlkIjogInJldm9rZWRfdXNlcl9pZDEiLA0K
+ICAgICAgICAgICAgImRvbWFpbiI6IHsNCiAgICAgICAgICAgICAgICAiaWQiOiAi
+ZG9tYWluX2lkMSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAiZG9tYWluX25h
+bWUxIg0KICAgICAgICAgICAgfQ0KICAgICAgICB9LA0KICAgICAgICAicm9sZXMi
+OiBbDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgIm5hbWUiOiAicm9s
+ZTEiDQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAg
+ICAgICJuYW1lIjogInJvbGUyIg0KICAgICAgICAgICAgfQ0KICAgICAgICBdLA0K
+ICAgICAgICAibWV0aG9kcyI6IFsNCiAgICAgICAgICAgICJwYXNzd29yZCINCiAg
+ICAgICAgXQ0KICAgIH0NCn0NCjGCAcowggHGAgEBMIGkMIGeMQowCAYDVQQFEwE1
+MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55dmFsZTES
+MBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMGCSqGSIb3
+DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2VsZiBTaWdu
+ZWQCAREwBwYFKw4DAhowDQYJKoZIhvcNAQEBBQAEggEAwFCjl3GSGrlil3cLwS11
+1gtc6K3gBSMbc7LviIFk4KDRBvHWEHT1fs/Q4T0Y12P97Uaxh47f2sNgdbsDKSE8
+K/KCeMy+0I7Eo3iDoXKcIRPux1sXFhOX36qLPpY4eWd3Q77MiUPng+78qA3AMPPl
+wEcfb2OaYsWmVi9jGsDfAvksF/WO5dg+G9m2l+zcboIJswsKbBJnM5bn8EDHk7bg
+YuMnOzqZsoymr6sehOPQ8QTV6kIj1w/gmtkaIH2QtBo78hCqjZ+cFeYy4zDk2HJg
+Mf7PDm0hx1G0hJMVxdNzkWoFvLreTzRselsrXrx8Gejof92JyKuBjZq0kBpphOHG
+6w==
+-----END CMS-----
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_revoked.pkiz b/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_revoked.pkiz
new file mode 100644 (file)
index 0000000..67823fd
--- /dev/null
@@ -0,0 +1 @@
+PKIZ_eJylVsmSozgQvesr5l7R0Symyhz6wG5oS5jFgLixtDEY7PLC-vUjYXd31Sw1PTOOcNgIZerle7no0yfykTXDRL8p0KMPnwA0zdWywNbXU2zuuwxJTqacyNpiUhRZXCqSow2KL63kYntRC6gYFVnfLQ3FOxuemfJAdbSVlNBFSSuK6PpttJiUu9VpaT6bq2uZrawuaYIqV-7PcSjscTPU8fzsjiAPt1dTsQ4px-6TcFHapfxiNsI-Dbfkv1TGhnjDYd1G3Lw2mGVfmE19MKsT-XU7kIb6a1qLr7GqlTuPvvxpnBtBi0OBeW_s1hmHxiSSmSQUW0A9pcfgmipvPB_dOm30NtffOkb73NCvKZdRlCkJlThna3A3iLt0Fdxiz6ThEGO3T7m6zVfw--Z9bLAEaeD5NHbFOuUrt7fLZQegb_LrSmqhshjsquDRhLu80jpUuSVq8BQ3VoWn7YRUyMb-fo8qucEcXtihVaIKDwBxWrlWpDJrgiON6Y7IqmOu7tKD2D5QvaYkrIzyo79HASiM_4MCUBg_UKyCMjXqKggseJdpz-Qr6Xk9LgdYZfSAfl1pz7aa8agUOegtOYAMk4srck6DKuRDBk5BbRsaB424iqtCwI3JoUrjsWeJEVXj6AqZ8ZC5Ea8kkdj6rm_Qxiu5S4juGSteye8lG0ms-i2nMn6X7Y4sv5L8qCg_4N_K9p6vwwhs36SE_WclwN95fuf4A3LBO3Z9U4Azu38mLAnZfcxtZ4ekIg-ZIVJEE4i44TVtbhP1HLKsuFbeV2PaiBz-IMXBr5FFk8uhIbVU-7fSg4-1n08e4zB_TbnFjOg70T4nzPIDUsItqfuRlO_1lzJQoRwthvWEGVzFDYBcXGIOsnByJhRuF9jHfdygxlbrElfkjZ_v50Q7yixpZa-Y_aVi-ut4_ypc8FGuY068kRxg_txo0I7kRZvwsARUjihirrTjEh5oV6LwLnFUT7nxIwv_Nt3BP0tI-dnyax5Pdy4eKV7ONh64SyRs0uaeZbQa44hW3hBsD_09C1cuk6mnbj1pIxqpIsS5f5oIJyxAI5FlnGH2eWiRMkb_ZMhCVepnREc2B_TUfFX3j9hfYzILcqNmvn1A3J03Nqe2ZLAETGKIh3vzIKPM0KeMz7usccpZlSZYZEY9xhHa4ciZkcFKmmyF6aHHDMDWnZHAGpB66hF7evQF8RpH8N0AefSILjXIhDr-VA08oI8pN9Sw_J4LwRRH5mNOut08_h7D9o3U8zwFhPXdvOhrDxWcPwzV-kD7A333xpiEFHcJFxxAxNPT7jDho3XFyvtNjz074pzAZ8WdbyhSduqLYmUAqdBkaBoH8v0GnVOvSFgNHEfXeo2FzrVXnPnZ0Hor2E7aGkoHQ2K3miJDxWG0AWiV5MgFCmQp85UAsWkjCDkpbRKSB2XpvnkPLZ-X67RGDA7RBbpar_az4zXQ-v36R977Wg0V-OP6Qm4vluTikIQhZDwhswmklDo63h2tG3EE8aRtoWzOJ0kDXG-54BqXsp-EeRuHjiKR0-Qe61_7hSrtT73qvL1PaTKQHXo30qTi8A1d3G3mrSX5pubCKREZlaxEeZF0qnqe3Gq0mmcvvB763tW0W69v-s-RDqpRgZnLY1x4BMViY3G8gDiW3cTRsolW2uc0MOVLyz_fal5dtTiSq7TstR2f2eNmoWKwQVmIxW25t-zzywnrqrEbO_VsuJd1bWtQ1vTyKWg3ngtbQfl80c8Xd0wydeAbqJRPVxcMHty3SBcuQd0vfX_h9ofRwuYUcmWwGJJ8SL7mJRwCzcebvLt5SqHwT_LGzgaxZ3aFBBzm5Ww_7faNib7K_nR4sXH7ujkdrPPlZSva8pNYtf1zPY0o6XtJv52T6LwNfIlbdkJvSQxA-XNVOzJ7Vlipvh6Dk_2UC0vmcxS3tiN9-QLmC62G1J-X298BCSOhiw==
\ No newline at end of file
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_scoped.json b/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_scoped.json
new file mode 100644 (file)
index 0000000..082c1b1
--- /dev/null
@@ -0,0 +1,120 @@
+{
+    "token": {
+        "methods": [
+            "password"
+        ],
+        "roles": [
+            {
+                "name": "role1"
+            },
+            {
+                "name": "role2"
+            }
+        ],
+        "expires_at": "2038-01-18T21:14:07Z",
+        "project": {
+            "id": "tenant_id1",
+            "domain": {
+                "id": "domain_id1",
+                "name": "domain_name1"
+            },
+            "enabled": true,
+            "description": null,
+            "name": "tenant_name1"
+        },
+        "catalog": [
+            {
+                "endpoints": [
+                    {
+                        "interface": "admin",
+                        "url": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne"
+                    },
+                    {
+                        "interface": "internal",
+                        "url": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne"
+                    },
+                    {
+                        "interface": "public",
+                        "url": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne"
+                    }
+                ],
+                "type": "volume",
+                "name": "volume"
+            },
+            {
+                "endpoints": [
+                    {
+                        "interface": "admin",
+                        "url": "http://127.0.0.1:9292/v1",
+                        "region": "regionOne"
+                    },
+                    {
+                        "interface": "internal",
+                        "url": "http://127.0.0.1:9292/v1",
+                        "region": "regionOne"
+                    },
+                    {
+                        "interface": "public",
+                        "url": "http://127.0.0.1:9292/v1",
+                        "region": "regionOne"
+                    }
+                ],
+                "type": "image",
+                "name": "glance"
+            },
+            {
+                "endpoints": [
+                    {
+                        "interface": "admin",
+                        "url": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne"
+                    },
+                    {
+                        "interface": "internal",
+                        "url": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne"
+                    },
+                    {
+                        "interface": "public",
+                        "url": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a",
+                        "region": "regionOne"
+                    }
+                ],
+                "type": "compute",
+                "name": "nova"
+            },
+            {
+                "endpoints": [
+                    {
+                        "interface": "admin",
+                        "url": "http://127.0.0.1:35357/v3",
+                        "region": "RegionOne"
+                    },
+                    {
+                        "interface": "internal",
+                        "url": "http://127.0.0.1:35357/v3",
+                        "region": "RegionOne"
+                    },
+                    {
+                        "interface": "public",
+                        "url": "http://127.0.0.1:5000/v3",
+                        "region": "RegionOne"
+                    }
+                ],
+                "type": "identity",
+                "name": "keystone"
+            }
+        ],
+        "user": {
+            "domain": {
+                "id": "domain_id1",
+                "name": "domain_name1"
+            },
+            "name": "user_name1",
+            "id": "user_id1"
+        }
+    }
+}
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_scoped.pem b/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_scoped.pem
new file mode 100644 (file)
index 0000000..e11cf03
--- /dev/null
@@ -0,0 +1,98 @@
+-----BEGIN CMS-----
+MIIR5gYJKoZIhvcNAQcCoIIR1zCCEdMCAQExCTAHBgUrDgMCGjCCD/MGCSqGSIb3
+DQEHAaCCD+QEgg/gew0KICAgICJ0b2tlbiI6IHsNCiAgICAgICAgIm1ldGhvZHMi
+OiBbDQogICAgICAgICAgICAicGFzc3dvcmQiDQogICAgICAgIF0sDQogICAgICAg
+ICJyb2xlcyI6IFsNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAibmFt
+ZSI6ICJyb2xlMSINCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7DQogICAg
+ICAgICAgICAgICAgIm5hbWUiOiAicm9sZTIiDQogICAgICAgICAgICB9DQogICAg
+ICAgIF0sDQogICAgICAgICJleHBpcmVzX2F0IjogIjIwMzgtMDEtMThUMjE6MTQ6
+MDdaIiwNCiAgICAgICAgInByb2plY3QiOiB7DQogICAgICAgICAgICAiaWQiOiAi
+dGVuYW50X2lkMSIsDQogICAgICAgICAgICAiZG9tYWluIjogew0KICAgICAgICAg
+ICAgICAgICJpZCI6ICJkb21haW5faWQxIiwNCiAgICAgICAgICAgICAgICAibmFt
+ZSI6ICJkb21haW5fbmFtZTEiDQogICAgICAgICAgICB9LA0KICAgICAgICAgICAg
+ImVuYWJsZWQiOiB0cnVlLA0KICAgICAgICAgICAgImRlc2NyaXB0aW9uIjogbnVs
+bCwNCiAgICAgICAgICAgICJuYW1lIjogInRlbmFudF9uYW1lMSINCiAgICAgICAg
+fSwNCiAgICAgICAgImNhdGFsb2ciOiBbDQogICAgICAgICAgICB7DQogICAgICAg
+ICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAgew0K
+ICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJhZG1pbiIsDQog
+ICAgICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6
+ODc3Ni92MS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIsDQogICAg
+ICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSINCiAgICAg
+ICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAg
+ICAgICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJpbnRlcm5hbCIsDQogICAg
+ICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3
+Ni92MS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIsDQogICAgICAg
+ICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSINCiAgICAgICAg
+ICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAgICAg
+ICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJwdWJsaWMiLA0KICAgICAgICAg
+ICAgICAgICAgICAgICAgInVybCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzYvdjEv
+NjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAg
+ICAgICAgICAgICAgInJlZ2lvbiI6ICJyZWdpb25PbmUiDQogICAgICAgICAgICAg
+ICAgICAgIH0NCiAgICAgICAgICAgICAgICBdLA0KICAgICAgICAgICAgICAgICJ0
+eXBlIjogInZvbHVtZSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAidm9sdW1l
+Ig0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAg
+ICAiZW5kcG9pbnRzIjogWw0KICAgICAgICAgICAgICAgICAgICB7DQogICAgICAg
+ICAgICAgICAgICAgICAgICAiaW50ZXJmYWNlIjogImFkbWluIiwNCiAgICAgICAg
+ICAgICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3Yx
+IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25l
+Ig0KICAgICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgICB7
+DQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJmYWNlIjogImludGVybmFs
+IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4w
+LjAuMTo5MjkyL3YxIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24i
+OiAicmVnaW9uT25lIg0KICAgICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAg
+ICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJmYWNl
+IjogInB1YmxpYyIsDQogICAgICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0
+dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQogICAgICAgICAgICAgICAgICAgICAg
+ICAicmVnaW9uIjogInJlZ2lvbk9uZSINCiAgICAgICAgICAgICAgICAgICAgfQ0K
+ICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgInR5cGUiOiAiaW1h
+Z2UiLA0KICAgICAgICAgICAgICAgICJuYW1lIjogImdsYW5jZSINCiAgICAgICAg
+ICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50
+cyI6IFsNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAg
+ICAgICAgImludGVyZmFjZSI6ICJhZG1pbiIsDQogICAgICAgICAgICAgICAgICAg
+ICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2Zi
+Y2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwNCiAgICAgICAgICAgICAgICAgICAg
+ICAgICJyZWdpb24iOiAicmVnaW9uT25lIg0KICAgICAgICAgICAgICAgICAgICB9
+LA0KICAgICAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICAg
+ICAiaW50ZXJmYWNlIjogImludGVybmFsIiwNCiAgICAgICAgICAgICAgICAgICAg
+ICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YxLjEvNjRiNmYzZmJj
+YzUzNDM1ZThhNjBmY2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAgICAgICAgICAg
+ICAgInJlZ2lvbiI6ICJyZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0s
+DQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAg
+ICJpbnRlcmZhY2UiOiAicHVibGljIiwNCiAgICAgICAgICAgICAgICAgICAgICAg
+ICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YxLjEvNjRiNmYzZmJjYzUz
+NDM1ZThhNjBmY2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAgICAgICAgICAgICAg
+InJlZ2lvbiI6ICJyZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0NCiAg
+ICAgICAgICAgICAgICBdLA0KICAgICAgICAgICAgICAgICJ0eXBlIjogImNvbXB1
+dGUiLA0KICAgICAgICAgICAgICAgICJuYW1lIjogIm5vdmEiDQogICAgICAgICAg
+ICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMi
+OiBbDQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAg
+ICAgICJpbnRlcmZhY2UiOiAiYWRtaW4iLA0KICAgICAgICAgICAgICAgICAgICAg
+ICAgInVybCI6ICJodHRwOi8vMTI3LjAuMC4xOjM1MzU3L3YzIiwNCiAgICAgICAg
+ICAgICAgICAgICAgICAgICJyZWdpb24iOiAiUmVnaW9uT25lIg0KICAgICAgICAg
+ICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgICB7DQogICAgICAgICAg
+ICAgICAgICAgICAgICAiaW50ZXJmYWNlIjogImludGVybmFsIiwNCiAgICAgICAg
+ICAgICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTozNTM1Ny92
+MyIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogIlJlZ2lvbk9u
+ZSINCiAgICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICAg
+ew0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJwdWJsaWMi
+LA0KICAgICAgICAgICAgICAgICAgICAgICAgInVybCI6ICJodHRwOi8vMTI3LjAu
+MC4xOjUwMDAvdjMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6
+ICJSZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAg
+ICAgICBdLA0KICAgICAgICAgICAgICAgICJ0eXBlIjogImlkZW50aXR5IiwNCiAg
+ICAgICAgICAgICAgICAibmFtZSI6ICJrZXlzdG9uZSINCiAgICAgICAgICAgIH0N
+CiAgICAgICAgXSwNCiAgICAgICAgInVzZXIiOiB7DQogICAgICAgICAgICAiZG9t
+YWluIjogew0KICAgICAgICAgICAgICAgICJpZCI6ICJkb21haW5faWQxIiwNCiAg
+ICAgICAgICAgICAgICAibmFtZSI6ICJkb21haW5fbmFtZTEiDQogICAgICAgICAg
+ICB9LA0KICAgICAgICAgICAgIm5hbWUiOiAidXNlcl9uYW1lMSIsDQogICAgICAg
+ICAgICAiaWQiOiAidXNlcl9pZDEiDQogICAgICAgIH0NCiAgICB9DQp9DQoxggHK
+MIIBxgIBATCBpDCBnjEKMAgGA1UEBRMBNTELMAkGA1UEBhMCVVMxCzAJBgNVBAgT
+AkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxEjAQBgNVBAoTCU9wZW5TdGFjazERMA8G
+A1UECxMIS2V5c3RvbmUxJTAjBgkqhkiG9w0BCQEWFmtleXN0b25lQG9wZW5zdGFj
+ay5vcmcxFDASBgNVBAMTC1NlbGYgU2lnbmVkAgERMAcGBSsOAwIaMA0GCSqGSIb3
+DQEBAQUABIIBAMq7ffe3ft88hD0EXJfWqkoEGcnal6NmTuLAiCOeQjDxR5TEIx0x
+HanKHWAG7Ko/97KgKAAFwOq3hhnbbKbKq7Z3brUNPXNRwBd3RusUrsLQOWwwKAsF
+acD8a4XXx6oC8dTsuFivDtMNb1JvBRIWcZXznOtn/bkFcvVhOQ+Af93c9xPBUpMq
+1667DbVKWRJEsMrcf5r7wYRQBtAKZU3CAjbNDighdTJWwF7TIWZycnF3OHYmu5J2
+wvcuB8ex+xRvf1lw1qnb3lC43A4M1KqhnHPpWUrpmAFnzAcYwc7ts2iCqD/UwVBP
+YcXU8kk8bY6leNJKR9xjHcIfW8SnREZVbXA=
+-----END CMS-----
diff --git a/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_scoped.pkiz b/keystonemiddleware-moon/examples/pki/cms/auth_v3_token_scoped.pkiz
new file mode 100644 (file)
index 0000000..d687c03
--- /dev/null
@@ -0,0 +1 @@
+PKIZ_eJy9V8lyo0gQvddXzN3RYUCgNoc-FItYTCGBEEvdWCwWscjWwvL1UyDJ3W577J6YiFGEDlUFmS_zvcxKvn0jP0FWNPMvEa3HxTeANE1X2kB_bLCWnWMTWrHYkD1JEkXRSkVoyZ3oQFVINy9SikSlEEWhTxVx_aystWgGJEtWYUg2u52cprH71OtUxBzLKNfmmnrIY1U_h5VbJOJljRX-GHjlSSualDx7AoFHl-NCq-xz5C32Ucmfxj201g6aqO_x-KKo7yKGzkKP24ae1Wk1NZ6VUbXIQaS4u9FAouo0XrfEEJdF3iZf5jBPFJcY4yifmfY6LR_P7TJmzD70BSr0-BMYX45q9xCJ42E5GdNqe7R-Shb8Hktyvh0N1_qZOBvGc292yMn5Ea-1OSBQ-ojpCGSdN0Th-68I4oo_YEfLt-4E-Yh9u4kY-2Kk19vANweQMHyPRT0xRJhjP2tDXy9jms-mpIlajyTEGg7sDEdmXUnrloVMLQs48_IpRwUIfDuLFL7-HRo5ZAgyekQzGfe4Xazw-6i2X8NIfP0ALgxkVKLCudE_dKiIT0hkW6OQ50spnpk5z6D1A2MqGoMLswoqszAdRKHBLZeKzACk4AIXKRdUGmMW8iy40kc8lXGFs4C55CPw7GPosROauHLrkYHROSDZLTFTnqMdf8K3RNZuH134bxLVbpf5wxk52swo4IiO5CGdmUNwTgr5DMzCzgmwAVd6EQybwZQQjZ0sMwuhCpiAXXp6bhZBZzLy5J2IicK-XgWeWV4QVOWJCKYnyTtMQrkhyicEPfaSfcRwq6jaTHog8qXjqp3CClWbArHUnI1B7s1-TByB6DSsOcSMMQs6YwiooMAVYnAeMIhGgzWY3oYNnKDFlVktpTIHQUGOnCS7yPSCBleL4kplm3j6IfTQu-TdkIJb8vxJrjYXK9c6ICpMxkIbC0d9o486UhankZ3RKPgngXyqD0fj0KQP7QD-DecfUQ6-5nzXE48j5_8fjRwiXkca_4QZ8FmMvzMTenSGmVckU-u7ViN3Tir507L9J1bAa9mKIy3sH6nvV_GBD2LMsELvo0vHuSaRvba4S7gOw70KDHwpmi_Qgc_gfYDuKjrSeMULQvAVxK8Qgv-SwBEh-Lfl-7tGwE0kAcMfY9Wl8AcXTMi4XDwjd2f1vsWDPy1hNPZLJyZFhd4UFbhVlVYtdtF4bb8vqPVnBQXGivqgoIg3RJs9SW7_8T1xRTDOB-37hJV_fleAn41j0yIJvovxNcRaoIOq2wf9W4mDEc7mjYDo6aZO1LK9qQ-TQSNRSurplT53wL5GQhlb2m20uc5Ev3Tf17Fm2nNuLc2acnCblYDPlLvLcAF_fZmOGbd_O9rcppfRu36dlWgebB1FhHHTpqmqwFQWkKbsyP8JWU0rwkByLWshtzKNrEMrWtNakVvd3QyygeBOAZDeyKKARIuSO7mAlpCargBjR3RNOqo4LiHNlsBfIwEq07MZ0p2ZUEalSZEEvwBky63UTpYNuc0M7JdDohD6HLlEInodKMk8qUM78H7K2oURMQSi-mLJqMisNMgbJGiTJ9ghY8O4B5wLTuglJ-xZIiTOhDZYPLasBLOmlaxABz9HXFkQLEimVRnmJ3OlLmcvbKSdqMYrmzCrm95WXJ12CpbiH4Ln1O5ZzC2aZ6DndyU-zU7DXS1QL_Ndjdd-JsAIqbs9v3To5N5fB9zLshOf-uql6beRHX3H4Xy_hxWW6AqsHh-d7_NktVXtxxXTR2yhoe3cWAcs_bxqnxTBqRUha-onmROWuZpIXC05Em0v1vaB1bI50P2ZKjyrfXi33B4XFO47K4lXsKyFx7vW2Id3ZyKK9OUQMH7ztHPNY-vcQ38ZZliW5ORlDQYlpPYnVmg1NNNgWvIzt33g7oXy0LVwkMU8rNSu3g6ORWFa9GAxHL1NWqSxkdqqeL4HK0GEBs73RVma-_uGClnlMehWZR49Gdvvq8UiiqvZ1jZ0-OMHmD4xZFP6-bnxN6RCLsw=
\ No newline at end of file
diff --git a/keystonemiddleware-moon/examples/pki/cms/revocation_list.der b/keystonemiddleware-moon/examples/pki/cms/revocation_list.der
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/keystonemiddleware-moon/examples/pki/cms/revocation_list.json b/keystonemiddleware-moon/examples/pki/cms/revocation_list.json
new file mode 100644 (file)
index 0000000..2c239e5
--- /dev/null
@@ -0,0 +1,20 @@
+{
+    "revoked": [
+        {
+            "expires": "2112-08-14T17:58:48Z",
+            "id": "dc57ea171d2f93e4ff5fa01fe5711f2a"
+        },
+        {
+            "expires": "2112-08-14T17:58:48Z",
+            "id": "4948fb46f88c41af90b65213a48baef7"
+        },
+        {
+            "expires": "2112-08-14T17:58:48Z",
+            "id": "dc57ea171d2f93e4ff5fa01fe5711f2a"
+        },
+        {
+            "expires": "2112-08-14T17:58:48Z",
+            "id": "4948fb46f88c41af90b65213a48baef7"
+        }
+    ]
+}
diff --git a/keystonemiddleware-moon/examples/pki/cms/revocation_list.pem b/keystonemiddleware-moon/examples/pki/cms/revocation_list.pem
new file mode 100644 (file)
index 0000000..a86d6d3
--- /dev/null
@@ -0,0 +1,24 @@
+-----BEGIN CMS-----
+MIIEGAYJKoZIhvcNAQcCoIIECTCCBAUCAQExCTAHBgUrDgMCGjCCAiUGCSqGSIb3
+DQEHAaCCAhYEggISew0KICAgICJyZXZva2VkIjogWw0KICAgICAgICB7DQogICAg
+ICAgICAgICAiZXhwaXJlcyI6ICIyMTEyLTA4LTE0VDE3OjU4OjQ4WiIsDQogICAg
+ICAgICAgICAiaWQiOiAiZGM1N2VhMTcxZDJmOTNlNGZmNWZhMDFmZTU3MTFmMmEi
+DQogICAgICAgIH0sDQogICAgICAgIHsNCiAgICAgICAgICAgICJleHBpcmVzIjog
+IjIxMTItMDgtMTRUMTc6NTg6NDhaIiwNCiAgICAgICAgICAgICJpZCI6ICI0OTQ4
+ZmI0NmY4OGM0MWFmOTBiNjUyMTNhNDhiYWVmNyINCiAgICAgICAgfSwNCiAgICAg
+ICAgew0KICAgICAgICAgICAgImV4cGlyZXMiOiAiMjExMi0wOC0xNFQxNzo1ODo0
+OFoiLA0KICAgICAgICAgICAgImlkIjogImRjNTdlYTE3MWQyZjkzZTRmZjVmYTAx
+ZmU1NzExZjJhIg0KICAgICAgICB9LA0KICAgICAgICB7DQogICAgICAgICAgICAi
+ZXhwaXJlcyI6ICIyMTEyLTA4LTE0VDE3OjU4OjQ4WiIsDQogICAgICAgICAgICAi
+aWQiOiAiNDk0OGZiNDZmODhjNDFhZjkwYjY1MjEzYTQ4YmFlZjciDQogICAgICAg
+IH0NCiAgICBdDQp9DQoxggHKMIIBxgIBATCBpDCBnjEKMAgGA1UEBRMBNTELMAkG
+A1UEBhMCVVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxEjAQBgNV
+BAoTCU9wZW5TdGFjazERMA8GA1UECxMIS2V5c3RvbmUxJTAjBgkqhkiG9w0BCQEW
+FmtleXN0b25lQG9wZW5zdGFjay5vcmcxFDASBgNVBAMTC1NlbGYgU2lnbmVkAgER
+MAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIIBAGMtzsHJdosl27LoRWYHGknORRWE
+K0E9a7Bm4ZDt0XiGn0opGWpXF3Kj+7q86Ph1qcG9vZy20e2V+8n5696//OgMGCZe
+QNbkOv70c0pkICMqczv4RaNF+UPetwDdv+p0WV8nLH5dDVc8Pp8B4T6fN6vXHXA2
+GMWxxn8SpF9bvP8S5VCAt7wsvmhWJpJVYe6bOdYzlhR0yLJzv4GvHtPVP+cBz6nS
+uJguvt77MfQU97pOaDbvfmsJRUf/L3Fd93KbgLTzFPEhddTs1oD9pSDckncnZwua
+9nIDn2iFNB/NfZrbqy+owM0Nt5j1m4dcPX/qm0J9DAhKGeDUbIu+81yL308=
+-----END CMS-----
diff --git a/keystonemiddleware-moon/examples/pki/cms/revocation_list.pkiz b/keystonemiddleware-moon/examples/pki/cms/revocation_list.pkiz
new file mode 100644 (file)
index 0000000..600fce0
--- /dev/null
@@ -0,0 +1 @@
+PKIZ_eJx9VEuPszgQvPMr9h6NQgIhk8N3MMaACTaBmJdvCZMxGMhjkgmPX79kRtq9rNYXq0ul6u7qVr-9Tc9EDqZ_QbJ_BW8KwdhiXe5tLxyXz4KCsICXCQstCMHYQRCiHjLgmiL-sgSBjpzwpHPg_ubs8VFTrBC54DCBsYqEsL3T4A0848_DMqmxvIhUu1c8K7tD5jXFgA0M8UAYGnwGdJ8hVUkspAUy1gMZ6mmF7xh6Vw5fRK_Ox1jjKerpaNekzVdkGau8zRe8RR1JeUNZ0SskzYd87218aK5xm-iF00wVkCqoQEUk6kmldgFUe2qHk9BlEVgXNbAvlQ9BdUjDSnkRqVWrgcOnn7eBVUpq2SWXdZfLfDGJjDkL9by1Gy6L6nPfianN5uSa16JNRuXVJ5a4Jww_iCUehEUxYYVBmTCoVR5w1QncNj9-4DaSlH00OUMaScNhSjIqnEUtl0mbM9DzNl7QEfVceiU-q3fs_r-BL_-U_zYQq8FUNm-xSttcDxyiktRuA2ZWVMaTCC2n6qo8TVqFDt4my9ReCHc77YTZC2wCBs2rBc2zRFsChAMWMTIjYlKGfALq37gkMElIr8AReKagiQkEAzU1SYQ7BHIrCUMXdQ37SFffp4yXRyfukQThL_fCYLzpeLpiyodjy8OIIgLef5RhT_B-mawKLXoe27j3GJCmqG9lXTmbTjVhiKZmHs0po-pxuWqU0PlRGn-EhtWzaIvetsD-NxNhcEGbo5OLeNmcj21SA_FKVjjm_h6ADh8UAtR_9npaaxOEMTAnLwBePp4BLmXIWNlG3VbvrrPtiQexUW7rJVjJVTHLKFesvvOb53c2y3nfroKr_4HPWybJU5LKEN9F1blaEoPLEt9um4GU7jwrV4_30NvPxp29rpSZE9w6fjULI9zSqsSXWt34unwcYvmpzz_XiIe0nEtSfz6-gVaWj2__0JzrPF0PCCzvtnI-rXdREidG9V7NbmsBV_6mymo9HLTrEoxi53yWtrEjc_U6DtJ71MbzfWfCehrqqf-qb0q011N5z0mktafnQvrah6d2TEBxvsEi0o7hw_LnxL3Gxs2AJyPULAcZZR0GOHJPZzRX6GXHb1Y-J5pO3aO8k1ulj14d6C75KgSo8sN8zOaD2Y1P9P2F_yg_dwhR69-b9Dc2l4GQ
\ No newline at end of file
diff --git a/keystonemiddleware-moon/examples/pki/gen_cmsz.py b/keystonemiddleware-moon/examples/pki/gen_cmsz.py
new file mode 100644 (file)
index 0000000..6840c08
--- /dev/null
@@ -0,0 +1,117 @@
+#!/usr/bin/python
+
+# 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.
+
+import json
+import os
+
+from keystoneclient.common import cms
+from keystoneclient import utils
+
+CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
+
+
+def make_filename(*args):
+    return os.path.join(CURRENT_DIR, *args)
+
+
+def generate_revocation_list():
+    REVOKED_TOKENS = ['auth_token_revoked', 'auth_v3_token_revoked']
+    revoked_list = []
+    for token in REVOKED_TOKENS:
+        with open(make_filename('cms', '%s.pkiz' % name), 'r') as f:
+            token_data = f.read()
+            id = utils.hash_signed_token(token_data.encode('utf-8'))
+            revoked_list.append({
+                'id': id,
+                "expires": "2112-08-14T17:58:48Z"
+            })
+        with open(make_filename('cms', '%s.pem' % name), 'r') as f:
+            pem_data = f.read()
+            token_data = cms.cms_to_token(pem_data).encode('utf-8')
+            id = utils.hash_signed_token(token_data)
+            revoked_list.append({
+                'id': id,
+                "expires": "2112-08-14T17:58:48Z"
+            })
+    revoked_json = json.dumps({"revoked": revoked_list})
+    with open(make_filename('cms', 'revocation_list.json'), 'w') as f:
+        f.write(revoked_json)
+    encoded = cms.pkiz_sign(revoked_json,
+                            SIGNING_CERT_FILE_NAME,
+                            SIGNING_KEY_FILE_NAME)
+    with open(make_filename('cms', 'revocation_list.pkiz'), 'w') as f:
+        f.write(encoded)
+
+    encoded = cms.cms_sign_data(revoked_json,
+                                SIGNING_CERT_FILE_NAME,
+                                SIGNING_KEY_FILE_NAME)
+    with open(make_filename('cms', 'revocation_list.pem'), 'w') as f:
+        f.write(encoded)
+
+
+CA_CERT_FILE_NAME = make_filename('certs', 'cacert.pem')
+SIGNING_CERT_FILE_NAME = make_filename('certs', 'signing_cert.pem')
+SIGNING_KEY_FILE_NAME = make_filename('private', 'signing_key.pem')
+EXAMPLE_TOKENS = ['auth_token_revoked',
+                  'auth_token_unscoped',
+                  'auth_token_scoped',
+                  'auth_token_scoped_expired',
+                  'auth_v3_token_scoped',
+                  'auth_v3_token_revoked']
+
+
+# Helper script to generate the sample data for testing
+# the signed tokens using the existing JSON data for the
+# MII-prefixed tokens.  Uses the keys and certificates
+# generated in gen_pki.sh.
+def generate_der_form(name):
+    derfile = make_filename('cms', '%s.der' % name)
+    with open(derfile, 'w') as f:
+        derform = cms.cms_sign_data(text,
+                                    SIGNING_CERT_FILE_NAME,
+                                    SIGNING_KEY_FILE_NAME, cms.PKIZ_CMS_FORM)
+        f.write(derform)
+
+for name in EXAMPLE_TOKENS:
+    json_file = make_filename('cms', name + '.json')
+    pkiz_file = make_filename('cms', name + '.pkiz')
+    with open(json_file, 'r') as f:
+        string_data = f.read()
+
+    # validate the JSON
+    try:
+        token_data = json.loads(string_data)
+    except ValueError as v:
+        raise SystemExit('%s while processing token data from %s: %s' %
+                         (v, json_file, string_data))
+
+    text = json.dumps(token_data).encode('utf-8')
+
+    # Uncomment to record the token uncompressed,
+    # useful for debugging
+    # generate_der_form(name)
+
+    encoded = cms.pkiz_sign(text,
+                            SIGNING_CERT_FILE_NAME,
+                            SIGNING_KEY_FILE_NAME)
+
+    # verify before writing
+    cms.pkiz_verify(encoded,
+                    SIGNING_CERT_FILE_NAME,
+                    CA_CERT_FILE_NAME)
+
+    with open(pkiz_file, 'w') as f:
+        f.write(encoded)
+
+    generate_revocation_list()
diff --git a/keystonemiddleware-moon/examples/pki/gen_pki.sh b/keystonemiddleware-moon/examples/pki/gen_pki.sh
new file mode 100755 (executable)
index 0000000..b8b28f9
--- /dev/null
@@ -0,0 +1,213 @@
+#!/bin/bash
+
+# Copyright 2012 OpenStack Foundation
+#
+# 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.
+
+# These functions generate the certificates and signed tokens for the tests.
+
+DIR=`dirname "$0"`
+CURRENT_DIR=`cd "$DIR" && pwd`
+CERTS_DIR=$CURRENT_DIR/certs
+PRIVATE_DIR=$CURRENT_DIR/private
+CMS_DIR=$CURRENT_DIR/cms
+
+
+function rm_old {
+  rm -rf $CERTS_DIR/*.pem
+  rm -rf $PRIVATE_DIR/*.pem
+}
+
+function cleanup {
+  rm -rf *.conf > /dev/null 2>&1
+  rm -rf index* > /dev/null 2>&1
+  rm -rf *.crt > /dev/null 2>&1
+  rm -rf newcerts > /dev/null 2>&1
+  rm -rf *.pem > /dev/null 2>&1
+  rm -rf serial* > /dev/null 2>&1
+}
+
+function generate_ca_conf {
+  echo '
+[ req ]
+default_bits            = 2048
+default_keyfile         = cakey.pem
+default_md              = default
+
+prompt                  = no
+distinguished_name      = ca_distinguished_name
+
+x509_extensions         = ca_extensions
+
+[ ca_distinguished_name ]
+serialNumber            = 5
+countryName             = US
+stateOrProvinceName     = CA
+localityName            = Sunnyvale
+organizationName        = OpenStack
+organizationalUnitName  = Keystone
+emailAddress            = keystone@openstack.org
+commonName              = Self Signed
+
+[ ca_extensions ]
+basicConstraints        = critical,CA:true
+' > ca.conf
+}
+
+function generate_ssl_req_conf {
+  echo '
+[ req ]
+default_bits            = 2048
+default_keyfile         = keystonekey.pem
+default_md              = default
+
+prompt                  = no
+distinguished_name      = distinguished_name
+
+[ distinguished_name ]
+countryName             = US
+stateOrProvinceName     = CA
+localityName            = Sunnyvale
+organizationName        = OpenStack
+organizationalUnitName  = Keystone
+commonName              = localhost
+emailAddress            = keystone@openstack.org
+' > ssl_req.conf
+}
+
+function generate_cms_signing_req_conf {
+  echo '
+[ req ]
+default_bits            = 2048
+default_keyfile         = keystonekey.pem
+default_md              = default
+
+prompt                  = no
+distinguished_name      = distinguished_name
+
+[ distinguished_name ]
+countryName             = US
+stateOrProvinceName     = CA
+localityName            = Sunnyvale
+organizationName        = OpenStack
+organizationalUnitName  = Keystone
+commonName              = Keystone
+emailAddress            = keystone@openstack.org
+' > cms_signing_req.conf
+}
+
+function generate_signing_conf {
+  echo '
+[ ca ]
+default_ca      = signing_ca
+
+[ signing_ca ]
+dir             = .
+database        = $dir/index.txt
+new_certs_dir   = $dir/newcerts
+
+certificate     = $dir/certs/cacert.pem
+serial          = $dir/serial
+private_key     = $dir/private/cakey.pem
+
+default_days            = 21360
+default_crl_days        = 30
+default_md              = default
+
+policy                  = policy_any
+
+[ policy_any ]
+countryName             = supplied
+stateOrProvinceName     = supplied
+localityName            = optional
+organizationName        = supplied
+organizationalUnitName  = supplied
+emailAddress            = supplied
+commonName              = supplied
+' > signing.conf
+}
+
+function setup {
+  touch index.txt
+  echo '10' > serial
+  generate_ca_conf
+  mkdir newcerts
+}
+
+function check_error {
+  if [ $1 != 0 ] ; then
+    echo "Failed! rc=${1}"
+    echo 'Bailing ...'
+    cleanup
+    exit $1
+  else
+    echo 'Done'
+  fi
+}
+
+function generate_ca {
+  echo 'Generating New CA Certificate ...'
+  openssl req -x509 -newkey rsa:2048 -days 21360 -out $CERTS_DIR/cacert.pem -keyout $PRIVATE_DIR/cakey.pem -outform PEM -config ca.conf -nodes
+  check_error $?
+}
+
+function ssl_cert_req {
+  echo 'Generating SSL Certificate Request ...'
+  generate_ssl_req_conf
+  openssl req -newkey rsa:2048 -keyout $PRIVATE_DIR/ssl_key.pem -keyform PEM -out ssl_req.pem -outform PEM -config ssl_req.conf -nodes
+  check_error $?
+  #openssl req -in req.pem -text -noout
+}
+
+function cms_signing_cert_req {
+  echo 'Generating CMS Signing Certificate Request ...'
+  generate_cms_signing_req_conf
+  openssl req -newkey rsa:2048 -keyout $PRIVATE_DIR/signing_key.pem -keyform PEM -out cms_signing_req.pem -outform PEM -config cms_signing_req.conf -nodes
+  check_error $?
+  #openssl req -in req.pem -text -noout
+}
+
+function issue_certs {
+  generate_signing_conf
+  echo 'Issuing SSL Certificate ...'
+  openssl ca -in ssl_req.pem -config signing.conf -batch
+  check_error $?
+  openssl x509 -in $CURRENT_DIR/newcerts/10.pem -out $CERTS_DIR/ssl_cert.pem
+  check_error $?
+  echo 'Issuing CMS Signing Certificate ...'
+  openssl ca -in cms_signing_req.pem -config signing.conf -batch
+  check_error $?
+  openssl x509 -in $CURRENT_DIR/newcerts/11.pem -out $CERTS_DIR/signing_cert.pem
+  check_error $?
+}
+
+function create_middleware_cert {
+  cp $CERTS_DIR/ssl_cert.pem $CERTS_DIR/middleware.pem
+  cat $PRIVATE_DIR/ssl_key.pem >> $CERTS_DIR/middleware.pem
+}
+
+function check_openssl {
+  echo 'Checking openssl availability ...'
+  which openssl
+  check_error $?
+}
+
+JSON_FILES="${CMS_DIR}/auth_token_revoked.json ${CMS_DIR}/auth_token_unscoped.json ${CMS_DIR}/auth_token_scoped.json ${CMS_DIR}/auth_token_scoped_expired.json ${CMS_DIR}/revocation_list.json ${CMS_DIR}/auth_v3_token_scoped.json ${CMS_DIR}/auth_v3_token_revoked.json"
+
+function gen_sample_cms {
+  for json_file in $JSON_FILES
+  do
+    openssl cms -sign -in $json_file -nosmimecap -signer $CERTS_DIR/signing_cert.pem -inkey $PRIVATE_DIR/signing_key.pem -outform PEM -nodetach -nocerts -noattr -out ${json_file/.json/.pem}
+  done
+}
+
diff --git a/keystonemiddleware-moon/examples/pki/private/cakey.pem b/keystonemiddleware-moon/examples/pki/private/cakey.pem
new file mode 100644 (file)
index 0000000..1c93ee1
--- /dev/null
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCl8906EaRpibQF
+cCBWfxzLi5x/XpZ9iL6UX92NrSJxcDbaGws7s+GtjgDy8UOEonesRWTeqQEZtHpC
+3/UHHOnsA8F6ha/pq9LioqT7RehCnZCLBJwh5Ct+lclpWs15SkjJD2LTDkjox0eA
+9nOBx+XDlWyU/GAyqx5Wsvg/Kxr0iod9/4IcJdnSdUjq4v0Cxg/zNk08XPJX+F0b
+UDhgdUf7JrAmmS5LA8wphRnbIgtVsf6VN9HrbqtHAJDxh8gEfuwdhEW1df1fBtZ+
+6WMIF3IRSbIsZELFB6sqcyRj7HhMoWMkdEyPb2f8mq61MzTgE6lJGIyTRvEoFie7
+qtGADIofAgMBAAECggEBAJ47X3y2xaU7f0KQHsVafgI2JAnuDl+zusOOhJlJs8Wl
+0Sc1EgjjAxOQiqcaE96rap//qqYDTuFLjCenkuItV32KNzizr3+GLZWaruRHS6X4
+xpFG2/gUrsQL3fdudOxpP+01lmzW+f25xRvZ4VilWRabquSDntWxA0R3cOwKFbGD
+uuwbTw3pBrRfCk/2IdpQtRrvvkVIFiYT6b/zeCQzhp4RETbC0oxqcEEOIUGmimAV
+9cbwafinxCo54cOfX4JAh3j7Mp3eQUymoFk5gnmIeVe0QmpH2VkN7eItrhEvHKOk
+On7a5xvQ8s3wqPV5ZawHQcqar/p3QnGkiT6a+8LkIMECgYEA2iJ2DprTGZFRN0M7
+Yj4WLsSC3/GKK8eYsKG3TvMrmPqUDaiWLIvBoc1Le59x9eoF7Mha+WX+cAFL+GTg
+1sB+PUZZStpf1R1tGvMldvpQ+5GplUBpuQe4J0n5rCG6+5jkvSr7xO+G1B+C3GFq
+KR3iltiW5WJRVwh2k8yGvx3agyUCgYEAwsKFX82F7O+9IVud1JSQWmZMiyEK+DEX
+JRnwx4HBuWr+AZqbb0grRRb6x8JTUOD4T7DZGxTaAdfzzRjKU2sBAO8VCgaj2Auv
+5nsbvfXvrmDDCqwoaD2PMy+kgFvE0QTh65tzuGXl1IgpIYSC1JwnP6kOeUDbqE+k
+UXzfVZzDdvMCgYByk9dfJIPt0h7O4Em4+NO+DQqRhtYE2PqjDM60cZZc7IIICp2X
+GHHFA4i6jq3Vde9WyIbAqYpUWtoExzgylTm6BdGxN7NOxf4hQcZUEHepLIHfG85s
+mlloibrTZ4RH06+SjZlhgE9Z7JNYHvMcVc5HXc0k/9ep15AxYiUFDjFQ4QKBgG7i
+k089U4/X2wWgBNdgkmN1tQTNllJCmNvdzhG41dQ8j0vYe8C7BS+76qJLCGaW/6lX
+lfRuRcUg78UI5UDjPloKxR7FMwmxdb+yvdPEr2bH3qQ36nWW/u30pSMTnJYownwD
+MLp/AYCk2U4lBNwJ3+rF1ODCRY2pcnOWtg0nSL5zAoGAWRoOinogEnOodJzO7eB3
+TmL6M9QMyrAPBDsCnduJ8yW5mMUNod139YbSDxZPYwTLhK/GiHP/7OvLV5hg0s4s
+QKnNaMeEowX7dyEO4ehnbfzysxXPKLRVhWhN6MCUc71NMxqr7QkuCXAjJS6/G21+
+Im3+Xb3Scq+UZghR+jiEZF0=
+-----END PRIVATE KEY-----
diff --git a/keystonemiddleware-moon/examples/pki/private/signing_key.pem b/keystonemiddleware-moon/examples/pki/private/signing_key.pem
new file mode 100644 (file)
index 0000000..758c0ff
--- /dev/null
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDM+VrILLl962VH
+S8EKWVzdkaOy0OoxGZ63gajM7VTm8AbgtVnYibIOnVZQuz1XbftIGNXPFhYNUypr
+LnMXrEEsnxgD4PvU/4bETG+stdricX6d1oKqsNFNR7F7zImiR/OzGhp7dONwccxf
+kfX4QHA5Ogso+XMfSdC72SRDszeCeGUcjuo/w2WSLW95SuVvcZLqE/pk3Q2TkCZ1
+8hvNfLoln43QpC469a7srUXATqOJ2mPNvL6E/wOyPefmAoCoG44lFoR3k2jZjBEI
+hstJxmH7XgvqErBzpcWd29dms8xz5PNwYdns9CIfb3GaHvQ6r5RTl37/avDrGHOW
+KOoD01xLAgMBAAECggEAaIi22qWsh+JYCW9B6NRAPyN6V8Sh2x6UykOO4cwb45b/
++vOh+YPn0fo9vfhvxTnq0A8SY4WBA5SpanYK7kTEDEyqw7em1y7l/RB6V5t7IMb+
+6uIuS3zXkVEB3AApJSEK0Ql7/gBTydHPh+H5jnzWfujyLhhhtNBBarvH+drZcWio
+lWx8RERN4cH+3DZD/xxjH2Ff+X1XMvb8Xcup7MlWi2FtREg7LttLNWNK25iWjciP
+QwfWQIrURRJrD2IrOr9V2nuIEvRqRRBoO+pxJT2sC48NJ3hiKV2GtSQe2nRpQJ47
+f9MEsF5KVQOOn+aQ60EKOI0MpNPmpiCZ5hFvBrNuOQKBgQD6vueEdI9eJgz5YN+t
+XWdpNippv35RTD8R4bQcE6GqIUXOmtQFS2wPJLn7nisZUsGMNEs36Yl0T9iow63r
+5GNAfgzpqN1XZqaSMwAdxKmlBNYpAkVXHhv+1jN+9diDYmoj9T+3Q6Zvk5e/Liyp
+6i+TsDppwmmr2utWajhyJ7owFwKBgQDRROncTztGDYLfRcrIoYsPo79KQ8tqwd2a
+07Usch2kplTqojCUmmhMMFgV2eZPPiCjnEy2bAYh9I/oj7xG6EwApXTshZdCpivC
+rbUV64MakRTUP8IvM6PdI+apkJRsRUi/bSyIbcRlvEoCMNZhfj/5VY6w/jlwrPJj
+oBOCXBlB7QKBgQDGEbEeX1i03UfYYh6uep7qbEAaooqsu5cCkBDPMO6+TmQvLPyY
+Zhio6bEEQs/2w/lhwBk+xHqw5zXVMiWbtiB03F1k4eBeXxbrW+AWo7gCQ4zMfh+6
+Dm284wVwn9D1D/OaDevT31uEvcjb2ySq3/PPLSEnU8xXVaoa6/NEsX8Q5wKBgQCm
+2smULWBXZKJ6n00mVxdnqun0rsVcI6Mrta14+KwGAdEnG5achdivFsTE924YtLKV
+gSPxN4RUQokTprc52jHvOf1WMNYAADpYCOSfy55G6nKvIP8VX5lB00Qw4uRUx5FP
+gB7H0K2NaGmiAYqNRXqAtOUG3kyyOFMzeAjWIdTJqQKBgQCHzY1c7sS1vv7mPEkr
+6CpwoaEbZeFnWoHBA8Rd82psqfYsVJIRwk5Id8zgDSEmoEi8hQ9UrYbrFpLK77xq
+EYSxLQHTNlM0G3lyEsv/gJhwYYhdTYiW3Cx3F6Y++jyn9O/+hFMyQvuesAL7DUYE
+ptEfvzFprpQUpByXkIpuJub6fg==
+-----END PRIVATE KEY-----
diff --git a/keystonemiddleware-moon/examples/pki/private/ssl_key.pem b/keystonemiddleware-moon/examples/pki/private/ssl_key.pem
new file mode 100644 (file)
index 0000000..363ce94
--- /dev/null
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDL06AaJROwHPgJ
+9tcySSBepzJ81jYars2sMvLjyuvdiIBbhWvbS/a9Tw3WgL8H6OALkHiOU/f0A6Rp
+v8dGDIDsxZQVjT/4SLaQUOeDM+9bfkKHpSd9G3CsdSSZgOH08n+MyZ7slPHfUHLY
+Wso0SJD0vAi1gmGDlSM/mmhhHTpCDGo6Wbwqare6JNeTCGJTJYwrxtoMCh/W1Zrs
+lPC5lFvlHD7KBBf6IU2A8Xh/dUa3p5pmQeHPW8Em90DzIB1qH0DRXl3KANc24xYR
+R45pPCVkk6vFsy6P0JwwpnkszB+LcK6CEsJhLsOYvQFsiQfSZ8m7YGhgrMLxtop4
+YEPirGGrAgMBAAECggEATwvbY0hNwlb5uqOIAXBqpUqiQdexU9fG26lGmSDxKBDv
+9o5frcRgBDrMWwvDCgY+HT4CAvB9kJx4/qnpVjkzJp/ZNiJ5VIiehIlbv348rXbh
+xkk+bz5dDATCFOXuu1fwL2FhyM5anwhMAav0DyK1VLQ3jGzr9GO6L8hqAn+bQFFu
+6ngiODwfhBMl5aRoL9UOBEhccK07znrH0JGRz+3+5Cdz59Xw91Bv210LhNNDL58+
+0JD0N+YztVOQd2bgwo0bQbOEijzmYq+0mjoqAnJh1/++y7PlIPs0AnPgqSnFPx9+
+6FsQEVRgk5Uq3kvPLaP4nT2y6MDZSp+ujYldvJhyQQKBgQDuX2pZIJMZ4aFnkG+K
+TmJ5wsLa/u9an0TmvAL9RLtBpVpQNKD8cQ+y8PUZavXDbAIt5NWqZVnTbCR79Dnd
+mZKblwcHhtsyA5f89el5KcxY2BREWdHdTnJpNd7XRlUECmzvX1zGj77lA982PhII
+yflRBRV3vqLkgC8vfoYgRyRElwKBgQDa5jnLdx/RahfYMOgn1HE5o4hMzLR4Y0Dd
++gELshcUbPqouoP5zOb8WOagVJIgZVOSN+/VqbilVYrqRiNTn2rnoxs+HHRdaJNN
+3eXllD4J2HfC2BIj1xSpIdyh2XewAJqw9IToHNB29QUhxOtgwseHciPG6JaKH2ik
+kqGKH/EKDQKBgFFAftygiOPCkCTgC9UmANUmOQsy6N2H+pF3tsEj43xt44oBVnqW
+A1boYXNnjRwuvdNs9BPf9i1l6E3EItFRXrLgWQoMwryakv0ryYh+YeRKyyW9RBbe
+fYs1TJ8unx4Ae79gTxxztQsVNcmkgLs0NWKTjAzEE3w14V+cDhYEie1DAoGBAJdI
+V5cLrBzBstsB6eBlDR9lqrRRIUS2a8U9m+1mVlcSfiWQSdehSd4K3tDdwePLw3ch
+W4qR8n+pYAlLEe0gFvUhn5lMdwt7U5qUCeehjUKmrRYm2FqWsbu2IFJnBjXIJSC4
+zQXRrC0aZ0KQYpAL7XPpaVp1slyhGmPqxuO78Y0dAoGBAMHo3EIMwu9rfuGwFodr
+GFsOZhfJqgo5GDNxxf89Q9WWpMDTCdX+wdBTrN/wsMbBuwIDHrUuRnk6D5CWRjSk
+/ikCgHN3kOtrbL8zzqRomGAIIWKYGFEIGe1GHVGo5r//HXHdPxFXygvruQ/xbOA4
+RGvmDiji8vVDq7Shho8I6KuT
+-----END PRIVATE KEY-----
diff --git a/keystonemiddleware-moon/examples/pki/run_all.sh b/keystonemiddleware-moon/examples/pki/run_all.sh
new file mode 100755 (executable)
index 0000000..ba2f0b6
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/bash -x
+
+# Copyright 2012 OpenStack Foundation
+#
+# 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.
+
+# This script generates the crypto necessary for the SSL tests.
+
+. gen_pki.sh
+
+check_openssl
+rm_old
+cleanup
+setup
+generate_ca
+ssl_cert_req
+cms_signing_cert_req
+issue_certs
+create_middleware_cert
+gen_sample_cms
+cleanup
diff --git a/keystonemiddleware-moon/keystonemiddleware.egg-info/PKG-INFO b/keystonemiddleware-moon/keystonemiddleware.egg-info/PKG-INFO
new file mode 100644 (file)
index 0000000..5eec4b0
--- /dev/null
@@ -0,0 +1,44 @@
+Metadata-Version: 1.1
+Name: keystonemiddleware
+Version: 1.4.1.dev34
+Summary: Middleware for OpenStack Identity
+Home-page: http://launchpad.net/keystonemiddleware
+Author: OpenStack
+Author-email: openstack-dev@lists.openstack.org
+License: UNKNOWN
+Description: Middleware for the OpenStack Identity API (Keystone)
+        ====================================================
+        
+        This package contains middleware modules designed to provide authentication and
+        authorization features to web services other than `Keystone
+        <https://github.com/openstack/keystone>`. The most prominent module is
+        ``keystonemiddleware.auth_token``. This package does not expose any CLI or
+        Python API features.
+        
+        The source is available on GitHub at:
+        
+            http://github.com/openstack/keystonemiddleware
+        
+        Bugs and feature requests are tracked on Launchpad at:
+        
+            https://bugs.launchpad.net/keystonemiddleware
+        
+        For any other information, refer to the parent project, Keystone:
+        
+            https://github.com/openstack/keystone
+        
+        For information on contributing, see ``CONTRIBUTING.rst``.
+        
+        
+Platform: UNKNOWN
+Classifier: Environment :: OpenStack
+Classifier: Intended Audience :: Information Technology
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
diff --git a/keystonemiddleware-moon/keystonemiddleware.egg-info/SOURCES.txt b/keystonemiddleware-moon/keystonemiddleware.egg-info/SOURCES.txt
new file mode 100644 (file)
index 0000000..6a6f64a
--- /dev/null
@@ -0,0 +1,104 @@
+.coveragerc
+.testr.conf
+CONTRIBUTING.rst
+HACKING.rst
+LICENSE
+MANIFEST.in
+README.rst
+babel.cfg
+openstack-common.conf
+requirements.txt
+setup.cfg
+setup.py
+test-requirements-py3.txt
+test-requirements.txt
+tox.ini
+doc/.gitignore
+doc/Makefile
+doc/ext/__init__.py
+doc/ext/apidoc.py
+doc/source/audit.rst
+doc/source/conf.py
+doc/source/index.rst
+doc/source/middlewarearchitecture.rst
+doc/source/images/audit.png
+doc/source/images/graphs_authComp.svg
+doc/source/images/graphs_authCompDelegate.svg
+examples/pki/gen_cmsz.py
+examples/pki/gen_pki.sh
+examples/pki/run_all.sh
+examples/pki/certs/cacert.pem
+examples/pki/certs/middleware.pem
+examples/pki/certs/signing_cert.pem
+examples/pki/certs/ssl_cert.pem
+examples/pki/cms/auth_token_revoked.json
+examples/pki/cms/auth_token_revoked.pem
+examples/pki/cms/auth_token_revoked.pkiz
+examples/pki/cms/auth_token_scoped.json
+examples/pki/cms/auth_token_scoped.pem
+examples/pki/cms/auth_token_scoped.pkiz
+examples/pki/cms/auth_token_scoped_expired.json
+examples/pki/cms/auth_token_scoped_expired.pem
+examples/pki/cms/auth_token_scoped_expired.pkiz
+examples/pki/cms/auth_token_unscoped.json
+examples/pki/cms/auth_token_unscoped.pem
+examples/pki/cms/auth_token_unscoped.pkiz
+examples/pki/cms/auth_v3_token_revoked.json
+examples/pki/cms/auth_v3_token_revoked.pem
+examples/pki/cms/auth_v3_token_revoked.pkiz
+examples/pki/cms/auth_v3_token_scoped.json
+examples/pki/cms/auth_v3_token_scoped.pem
+examples/pki/cms/auth_v3_token_scoped.pkiz
+examples/pki/cms/revocation_list.der
+examples/pki/cms/revocation_list.json
+examples/pki/cms/revocation_list.pem
+examples/pki/cms/revocation_list.pkiz
+examples/pki/private/cakey.pem
+examples/pki/private/signing_key.pem
+examples/pki/private/ssl_key.pem
+keystonemiddleware/__init__.py
+keystonemiddleware/audit.py
+keystonemiddleware/authz.py
+keystonemiddleware/ec2_token.py
+keystonemiddleware/i18n.py
+keystonemiddleware/opts.py
+keystonemiddleware/s3_token.py
+keystonemiddleware.egg-info/PKG-INFO
+keystonemiddleware.egg-info/SOURCES.txt
+keystonemiddleware.egg-info/dependency_links.txt
+keystonemiddleware.egg-info/entry_points.txt
+keystonemiddleware.egg-info/not-zip-safe
+keystonemiddleware.egg-info/pbr.json
+keystonemiddleware.egg-info/requires.txt
+keystonemiddleware.egg-info/top_level.txt
+keystonemiddleware/auth_token/__init__.py
+keystonemiddleware/auth_token/_auth.py
+keystonemiddleware/auth_token/_base.py
+keystonemiddleware/auth_token/_cache.py
+keystonemiddleware/auth_token/_exceptions.py
+keystonemiddleware/auth_token/_identity.py
+keystonemiddleware/auth_token/_memcache_crypt.py
+keystonemiddleware/auth_token/_memcache_pool.py
+keystonemiddleware/auth_token/_revocations.py
+keystonemiddleware/auth_token/_signing_dir.py
+keystonemiddleware/auth_token/_user_plugin.py
+keystonemiddleware/auth_token/_utils.py
+keystonemiddleware/openstack/__init__.py
+keystonemiddleware/openstack/common/__init__.py
+keystonemiddleware/openstack/common/memorycache.py
+keystonemiddleware/tests/__init__.py
+keystonemiddleware/tests/unit/__init__.py
+keystonemiddleware/tests/unit/client_fixtures.py
+keystonemiddleware/tests/unit/test_audit_middleware.py
+keystonemiddleware/tests/unit/test_opts.py
+keystonemiddleware/tests/unit/test_s3_token_middleware.py
+keystonemiddleware/tests/unit/utils.py
+keystonemiddleware/tests/unit/auth_token/__init__.py
+keystonemiddleware/tests/unit/auth_token/test_auth.py
+keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py
+keystonemiddleware/tests/unit/auth_token/test_connection_pool.py
+keystonemiddleware/tests/unit/auth_token/test_memcache_crypt.py
+keystonemiddleware/tests/unit/auth_token/test_revocations.py
+keystonemiddleware/tests/unit/auth_token/test_signing_dir.py
+keystonemiddleware/tests/unit/auth_token/test_utils.py
+tools/install_venv_common.py
\ No newline at end of file
diff --git a/keystonemiddleware-moon/keystonemiddleware.egg-info/dependency_links.txt b/keystonemiddleware-moon/keystonemiddleware.egg-info/dependency_links.txt
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/keystonemiddleware-moon/keystonemiddleware.egg-info/entry_points.txt b/keystonemiddleware-moon/keystonemiddleware.egg-info/entry_points.txt
new file mode 100644 (file)
index 0000000..8bc8336
--- /dev/null
@@ -0,0 +1,3 @@
+[oslo.config.opts]
+keystonemiddleware.auth_token = keystonemiddleware.opts:list_auth_token_opts
+
diff --git a/keystonemiddleware-moon/keystonemiddleware.egg-info/not-zip-safe b/keystonemiddleware-moon/keystonemiddleware.egg-info/not-zip-safe
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/keystonemiddleware-moon/keystonemiddleware.egg-info/pbr.json b/keystonemiddleware-moon/keystonemiddleware.egg-info/pbr.json
new file mode 100644 (file)
index 0000000..1a4827f
--- /dev/null
@@ -0,0 +1 @@
+{"is_release": false, "git_version": "6b3c86a"}
\ No newline at end of file
diff --git a/keystonemiddleware-moon/keystonemiddleware.egg-info/requires.txt b/keystonemiddleware-moon/keystonemiddleware.egg-info/requires.txt
new file mode 100644 (file)
index 0000000..392be97
--- /dev/null
@@ -0,0 +1,13 @@
+Babel>=1.3
+iso8601>=0.1.9
+oslo.config>=1.9.0  # Apache-2.0
+oslo.context>=0.2.0                     # Apache-2.0
+oslo.i18n>=1.3.0  # Apache-2.0
+oslo.serialization>=1.2.0               # Apache-2.0
+oslo.utils>=1.2.0                       # Apache-2.0
+pbr>=0.6,!=0.7,<1.0
+pycadf>=0.8.0
+python-keystoneclient>=1.1.0
+requests>=2.2.0,!=2.4.0
+six>=1.9.0
+WebOb>=1.2.3
\ No newline at end of file
diff --git a/keystonemiddleware-moon/keystonemiddleware.egg-info/top_level.txt b/keystonemiddleware-moon/keystonemiddleware.egg-info/top_level.txt
new file mode 100644 (file)
index 0000000..0622f2e
--- /dev/null
@@ -0,0 +1 @@
+keystonemiddleware
diff --git a/keystonemiddleware-moon/keystonemiddleware/__init__.py b/keystonemiddleware-moon/keystonemiddleware/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/keystonemiddleware-moon/keystonemiddleware/audit.py b/keystonemiddleware-moon/keystonemiddleware/audit.py
new file mode 100644 (file)
index 0000000..f44da80
--- /dev/null
@@ -0,0 +1,430 @@
+#
+# 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.
+
+"""
+Build open standard audit information based on incoming requests
+
+AuditMiddleware filter should be placed after keystonemiddleware.auth_token
+in the pipeline so that it can utilise the information the Identity server
+provides.
+"""
+
+import ast
+import collections
+import functools
+import logging
+import os.path
+import re
+import sys
+
+from oslo_config import cfg
+from oslo_context import context
+try:
+    import oslo.messaging
+    messaging = True
+except ImportError:
+    messaging = False
+from pycadf import cadftaxonomy as taxonomy
+from pycadf import cadftype
+from pycadf import credential
+from pycadf import endpoint
+from pycadf import eventfactory as factory
+from pycadf import host
+from pycadf import identifier
+from pycadf import reason
+from pycadf import reporterstep
+from pycadf import resource
+from pycadf import tag
+from pycadf import timestamp
+from six.moves import configparser
+from six.moves.urllib import parse as urlparse
+import webob.dec
+
+from keystonemiddleware.i18n import _LE, _LI
+
+
+_LOG = None
+
+
+def _log_and_ignore_error(fn):
+    @functools.wraps(fn)
+    def wrapper(*args, **kwargs):
+        try:
+            return fn(*args, **kwargs)
+        except Exception as e:
+            _LOG.exception(_LE('An exception occurred processing '
+                               'the API call: %s '), e)
+    return wrapper
+
+
+Service = collections.namedtuple('Service',
+                                 ['id', 'name', 'type', 'admin_endp',
+                                  'public_endp', 'private_endp'])
+
+
+AuditMap = collections.namedtuple('AuditMap',
+                                  ['path_kw',
+                                   'custom_actions',
+                                   'service_endpoints',
+                                   'default_target_endpoint_type'])
+
+
+class OpenStackAuditApi(object):
+
+    def __init__(self, cfg_file):
+        """Configure to recognize and map known api paths."""
+        path_kw = {}
+        custom_actions = {}
+        endpoints = {}
+        default_target_endpoint_type = None
+
+        if cfg_file:
+            try:
+                map_conf = configparser.SafeConfigParser()
+                map_conf.readfp(open(cfg_file))
+
+                try:
+                    default_target_endpoint_type = map_conf.get(
+                        'DEFAULT', 'target_endpoint_type')
+                except configparser.NoOptionError:
+                    pass
+
+                try:
+                    custom_actions = dict(map_conf.items('custom_actions'))
+                except configparser.Error:
+                    pass
+
+                try:
+                    path_kw = dict(map_conf.items('path_keywords'))
+                except configparser.Error:
+                    pass
+
+                try:
+                    endpoints = dict(map_conf.items('service_endpoints'))
+                except configparser.Error:
+                    pass
+            except configparser.ParsingError as err:
+                raise PycadfAuditApiConfigError(
+                    'Error parsing audit map file: %s' % err)
+        self._MAP = AuditMap(
+            path_kw=path_kw, custom_actions=custom_actions,
+            service_endpoints=endpoints,
+            default_target_endpoint_type=default_target_endpoint_type)
+
+    @staticmethod
+    def _clean_path(value):
+        """Clean path if path has json suffix."""
+        return value[:-5] if value.endswith('.json') else value
+
+    def get_action(self, req):
+        """Take a given Request, parse url path to calculate action type.
+
+        Depending on req.method:
+        if POST: path ends with 'action', read the body and use as action;
+                 path ends with known custom_action, take action from config;
+                 request ends with known path, assume is create action;
+                 request ends with unknown path, assume is update action.
+        if GET: request ends with known path, assume is list action;
+                request ends with unknown path, assume is read action.
+        if PUT, assume update action.
+        if DELETE, assume delete action.
+        if HEAD, assume read action.
+
+        """
+        path = req.path[:-1] if req.path.endswith('/') else req.path
+        url_ending = self._clean_path(path[path.rfind('/') + 1:])
+        method = req.method
+
+        if url_ending + '/' + method.lower() in self._MAP.custom_actions:
+            action = self._MAP.custom_actions[url_ending + '/' +
+                                              method.lower()]
+        elif url_ending in self._MAP.custom_actions:
+            action = self._MAP.custom_actions[url_ending]
+        elif method == 'POST':
+            if url_ending == 'action':
+                try:
+                    if req.json:
+                        body_action = list(req.json.keys())[0]
+                        action = taxonomy.ACTION_UPDATE + '/' + body_action
+                    else:
+                        action = taxonomy.ACTION_CREATE
+                except ValueError:
+                    action = taxonomy.ACTION_CREATE
+            elif url_ending not in self._MAP.path_kw:
+                action = taxonomy.ACTION_UPDATE
+            else:
+                action = taxonomy.ACTION_CREATE
+        elif method == 'GET':
+            if url_ending in self._MAP.path_kw:
+                action = taxonomy.ACTION_LIST
+            else:
+                action = taxonomy.ACTION_READ
+        elif method == 'PUT' or method == 'PATCH':
+            action = taxonomy.ACTION_UPDATE
+        elif method == 'DELETE':
+            action = taxonomy.ACTION_DELETE
+        elif method == 'HEAD':
+            action = taxonomy.ACTION_READ
+        else:
+            action = taxonomy.UNKNOWN
+
+        return action
+
+    def _get_service_info(self, endp):
+        service = Service(
+            type=self._MAP.service_endpoints.get(
+                endp['type'],
+                taxonomy.UNKNOWN),
+            name=endp['name'],
+            id=identifier.norm_ns(endp['endpoints'][0].get('id',
+                                                           endp['name'])),
+            admin_endp=endpoint.Endpoint(
+                name='admin',
+                url=endp['endpoints'][0]['adminURL']),
+            private_endp=endpoint.Endpoint(
+                name='private',
+                url=endp['endpoints'][0]['internalURL']),
+            public_endp=endpoint.Endpoint(
+                name='public',
+                url=endp['endpoints'][0]['publicURL']))
+
+        return service
+
+    def _build_typeURI(self, req, service_type):
+        """Build typeURI of target
+
+        Combines service type and corresponding path for greater detail.
+        """
+        type_uri = ''
+        prev_key = None
+        for key in re.split('/', req.path):
+            key = self._clean_path(key)
+            if key in self._MAP.path_kw:
+                type_uri += '/' + key
+            elif prev_key in self._MAP.path_kw:
+                type_uri += '/' + self._MAP.path_kw[prev_key]
+            prev_key = key
+        return service_type + type_uri
+
+    def _build_target(self, req, service):
+        """Build target resource."""
+        target_typeURI = (
+            self._build_typeURI(req, service.type)
+            if service.type != taxonomy.UNKNOWN else service.type)
+        target = resource.Resource(typeURI=target_typeURI,
+                                   id=service.id, name=service.name)
+        if service.admin_endp:
+            target.add_address(service.admin_endp)
+        if service.private_endp:
+            target.add_address(service.private_endp)
+        if service.public_endp:
+            target.add_address(service.public_endp)
+        return target
+
+    def get_target_resource(self, req):
+        """Retrieve target information
+
+        If discovery is enabled, target will attempt to retrieve information
+        from service catalog. If not, the information will be taken from
+        given config file.
+        """
+        service_info = Service(type=taxonomy.UNKNOWN, name=taxonomy.UNKNOWN,
+                               id=taxonomy.UNKNOWN, admin_endp=None,
+                               private_endp=None, public_endp=None)
+        try:
+            catalog = ast.literal_eval(
+                req.environ['HTTP_X_SERVICE_CATALOG'])
+        except KeyError:
+            raise PycadfAuditApiConfigError(
+                'Service catalog is missing. '
+                'Cannot discover target information')
+
+        default_endpoint = None
+        for endp in catalog:
+            admin_urlparse = urlparse.urlparse(
+                endp['endpoints'][0]['adminURL'])
+            public_urlparse = urlparse.urlparse(
+                endp['endpoints'][0]['publicURL'])
+            req_url = urlparse.urlparse(req.host_url)
+            if (req_url.netloc == admin_urlparse.netloc
+                    or req_url.netloc == public_urlparse.netloc):
+                service_info = self._get_service_info(endp)
+                break
+            elif (self._MAP.default_target_endpoint_type and
+                  endp['type'] == self._MAP.default_target_endpoint_type):
+                default_endpoint = endp
+        else:
+            if default_endpoint:
+                service_info = self._get_service_info(default_endpoint)
+        return self._build_target(req, service_info)
+
+
+class ClientResource(resource.Resource):
+    def __init__(self, project_id=None, **kwargs):
+        super(ClientResource, self).__init__(**kwargs)
+        if project_id is not None:
+            self.project_id = project_id
+
+
+class KeystoneCredential(credential.Credential):
+    def __init__(self, identity_status=None, **kwargs):
+        super(KeystoneCredential, self).__init__(**kwargs)
+        if identity_status is not None:
+            self.identity_status = identity_status
+
+
+class PycadfAuditApiConfigError(Exception):
+    """Error raised when pyCADF fails to configure correctly."""
+
+
+class AuditMiddleware(object):
+    """Create an audit event based on request/response.
+
+    The audit middleware takes in various configuration options such as the
+    ability to skip audit of certain requests. The full list of options can
+    be discovered here:
+    http://docs.openstack.org/developer/keystonemiddleware/audit.html
+    """
+
+    @staticmethod
+    def _get_aliases(proj):
+        aliases = {}
+        if proj:
+            # Aliases to support backward compatibility
+            aliases = {
+                '%s.openstack.common.rpc.impl_kombu' % proj: 'rabbit',
+                '%s.openstack.common.rpc.impl_qpid' % proj: 'qpid',
+                '%s.openstack.common.rpc.impl_zmq' % proj: 'zmq',
+                '%s.rpc.impl_kombu' % proj: 'rabbit',
+                '%s.rpc.impl_qpid' % proj: 'qpid',
+                '%s.rpc.impl_zmq' % proj: 'zmq',
+            }
+        return aliases
+
+    def __init__(self, app, **conf):
+        self._application = app
+        global _LOG
+        _LOG = logging.getLogger(conf.get('log_name', __name__))
+        self._service_name = conf.get('service_name')
+        self._ignore_req_list = [x.upper().strip() for x in
+                                 conf.get('ignore_req_list', '').split(',')]
+        self._cadf_audit = OpenStackAuditApi(conf.get('audit_map_file'))
+
+        transport_aliases = self._get_aliases(cfg.CONF.project)
+        if messaging:
+            self._notifier = oslo.messaging.Notifier(
+                oslo.messaging.get_transport(cfg.CONF,
+                                             aliases=transport_aliases),
+                os.path.basename(sys.argv[0]))
+
+    def _emit_audit(self, context, event_type, payload):
+        """Emit audit notification
+
+        if oslo.messaging enabled, send notification. if not, log event.
+        """
+
+        if messaging:
+            self._notifier.info(context, event_type, payload)
+        else:
+            _LOG.info(_LI('Event type: %(event_type)s, Context: %(context)s, '
+                          'Payload: %(payload)s'), {'context': context,
+                                                    'event_type': event_type,
+                                                    'payload': payload})
+
+    def _create_event(self, req):
+        correlation_id = identifier.generate_uuid()
+        action = self._cadf_audit.get_action(req)
+
+        initiator = ClientResource(
+            typeURI=taxonomy.ACCOUNT_USER,
+            id=identifier.norm_ns(str(req.environ['HTTP_X_USER_ID'])),
+            name=req.environ['HTTP_X_USER_NAME'],
+            host=host.Host(address=req.client_addr, agent=req.user_agent),
+            credential=KeystoneCredential(
+                token=req.environ['HTTP_X_AUTH_TOKEN'],
+                identity_status=req.environ['HTTP_X_IDENTITY_STATUS']),
+            project_id=identifier.norm_ns(req.environ['HTTP_X_PROJECT_ID']))
+        target = self._cadf_audit.get_target_resource(req)
+
+        event = factory.EventFactory().new_event(
+            eventType=cadftype.EVENTTYPE_ACTIVITY,
+            outcome=taxonomy.OUTCOME_PENDING,
+            action=action,
+            initiator=initiator,
+            target=target,
+            observer=resource.Resource(id='target'))
+        event.requestPath = req.path_qs
+        event.add_tag(tag.generate_name_value_tag('correlation_id',
+                                                  correlation_id))
+        # cache model in request to allow tracking of transistive steps.
+        req.environ['cadf_event'] = event
+        return event
+
+    @_log_and_ignore_error
+    def _process_request(self, request):
+        event = self._create_event(request)
+
+        self._emit_audit(context.get_admin_context().to_dict(),
+                         'audit.http.request', event.as_dict())
+
+    @_log_and_ignore_error
+    def _process_response(self, request, response=None):
+        # NOTE(gordc): handle case where error processing request
+        if 'cadf_event' not in request.environ:
+            self._create_event(request)
+        event = request.environ['cadf_event']
+
+        if response:
+            if response.status_int >= 200 and response.status_int < 400:
+                result = taxonomy.OUTCOME_SUCCESS
+            else:
+                result = taxonomy.OUTCOME_FAILURE
+            event.reason = reason.Reason(
+                reasonType='HTTP', reasonCode=str(response.status_int))
+        else:
+            result = taxonomy.UNKNOWN
+
+        event.outcome = result
+        event.add_reporterstep(
+            reporterstep.Reporterstep(
+                role=cadftype.REPORTER_ROLE_MODIFIER,
+                reporter=resource.Resource(id='target'),
+                reporterTime=timestamp.get_utc_now()))
+
+        self._emit_audit(context.get_admin_context().to_dict(),
+                         'audit.http.response', event.as_dict())
+
+    @webob.dec.wsgify
+    def __call__(self, req):
+        if req.method in self._ignore_req_list:
+            return req.get_response(self._application)
+
+        self._process_request(req)
+        try:
+            response = req.get_response(self._application)
+        except Exception:
+            self._process_response(req)
+            raise
+        else:
+            self._process_response(req, response)
+        return response
+
+
+def filter_factory(global_conf, **local_conf):
+    """Returns a WSGI filter app for use with paste.deploy."""
+    conf = global_conf.copy()
+    conf.update(local_conf)
+
+    def audit_filter(app):
+        return AuditMiddleware(app, **conf)
+    return audit_filter
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/__init__.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/__init__.py
new file mode 100644 (file)
index 0000000..8053971
--- /dev/null
@@ -0,0 +1,1171 @@
+# Copyright 2010-2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Token-based Authentication Middleware
+
+This WSGI component:
+
+* Verifies that incoming client requests have valid tokens by validating
+  tokens with the auth service.
+* Rejects unauthenticated requests unless the auth_token middleware is in
+  'delay_auth_decision' mode, which means the final decision is delegated to
+  the downstream WSGI component (usually the OpenStack service).
+* Collects and forwards identity information based on a valid token
+  such as user name, tenant, etc
+
+Refer to: http://docs.openstack.org/developer/keystonemiddleware/\
+middlewarearchitecture.html
+
+
+Echo test server
+----------------
+
+Run this module directly to start a protected echo service on port 8000::
+
+ $ python -m keystonemiddleware.auth_token
+
+When the ``auth_token`` module authenticates a request, the echo service
+will respond with all the environment variables presented to it by this
+module.
+
+
+Headers
+-------
+
+The auth_token middleware uses headers sent in by the client on the request
+and sets headers and environment variables for the downstream WSGI component.
+
+Coming in from initial call from client or customer
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+HTTP_X_AUTH_TOKEN
+    The client token being passed in.
+
+HTTP_X_SERVICE_TOKEN
+    A service token being passed in.
+
+Used for communication between components
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+WWW-Authenticate
+    HTTP header returned to a user indicating which endpoint to use
+    to retrieve a new token
+
+What auth_token adds to the request for use by the OpenStack service
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When using composite authentication (a user and service token are
+present) additional service headers relating to the service user
+will be added. They take the same form as the standard headers but add
+'_SERVICE_'. These headers will not exist in the environment if no
+service token is present.
+
+HTTP_X_IDENTITY_STATUS, HTTP_X_SERVICE_IDENTITY_STATUS
+    'Confirmed' or 'Invalid'
+    The underlying service will only see a value of 'Invalid' if the Middleware
+    is configured to run in 'delay_auth_decision' mode. As with all such
+    headers, HTTP_X_SERVICE_IDENTITY_STATUS will only exist in the
+    environment if a service token is presented. This is different than
+    HTTP_X_IDENTITY_STATUS which is always set even if no user token is
+    presented. This allows the underlying service to determine if a
+    denial should use 401 or 403.
+
+HTTP_X_DOMAIN_ID, HTTP_X_SERVICE_DOMAIN_ID
+    Identity service managed unique identifier, string. Only present if
+    this is a domain-scoped v3 token.
+
+HTTP_X_DOMAIN_NAME, HTTP_X_SERVICE_DOMAIN_NAME
+    Unique domain name, string. Only present if this is a domain-scoped
+    v3 token.
+
+HTTP_X_PROJECT_ID, HTTP_X_SERVICE_PROJECT_ID
+    Identity service managed unique identifier, string. Only present if
+    this is a project-scoped v3 token, or a tenant-scoped v2 token.
+
+HTTP_X_PROJECT_NAME, HTTP_X_SERVICE_PROJECT_NAME
+    Project name, unique within owning domain, string. Only present if
+    this is a project-scoped v3 token, or a tenant-scoped v2 token.
+
+HTTP_X_PROJECT_DOMAIN_ID, HTTP_X_SERVICE_PROJECT_DOMAIN_ID
+    Identity service managed unique identifier of owning domain of
+    project, string.  Only present if this is a project-scoped v3 token. If
+    this variable is set, this indicates that the PROJECT_NAME can only
+    be assumed to be unique within this domain.
+
+HTTP_X_PROJECT_DOMAIN_NAME, HTTP_X_SERVICE_PROJECT_DOMAIN_NAME
+    Name of owning domain of project, string. Only present if this is a
+    project-scoped v3 token. If this variable is set, this indicates that
+    the PROJECT_NAME can only be assumed to be unique within this domain.
+
+HTTP_X_USER_ID, HTTP_X_SERVICE_USER_ID
+    Identity-service managed unique identifier, string
+
+HTTP_X_USER_NAME, HTTP_X_SERVICE_USER_NAME
+    User identifier, unique within owning domain, string
+
+HTTP_X_USER_DOMAIN_ID, HTTP_X_SERVICE_USER_DOMAIN_ID
+    Identity service managed unique identifier of owning domain of
+    user, string. If this variable is set, this indicates that the USER_NAME
+    can only be assumed to be unique within this domain.
+
+HTTP_X_USER_DOMAIN_NAME, HTTP_X_SERVICE_USER_DOMAIN_NAME
+    Name of owning domain of user, string. If this variable is set, this
+    indicates that the USER_NAME can only be assumed to be unique within
+    this domain.
+
+HTTP_X_ROLES, HTTP_X_SERVICE_ROLES
+    Comma delimited list of case-sensitive role names
+
+HTTP_X_SERVICE_CATALOG
+    json encoded service catalog (optional).
+    For compatibility reasons this catalog will always be in the V2 catalog
+    format even if it is a v3 token.
+
+    Note: This is an exception in that it contains 'SERVICE' but relates to a
+          user token, not a service token. The existing user's
+          catalog can be very large; it was decided not to present a catalog
+          relating to the service token to avoid using more HTTP header space.
+
+HTTP_X_TENANT_ID
+    *Deprecated* in favor of HTTP_X_PROJECT_ID
+    Identity service managed unique identifier, string. For v3 tokens, this
+    will be set to the same value as HTTP_X_PROJECT_ID
+
+HTTP_X_TENANT_NAME
+    *Deprecated* in favor of HTTP_X_PROJECT_NAME
+    Project identifier, unique within owning domain, string. For v3 tokens,
+    this will be set to the same value as HTTP_X_PROJECT_NAME
+
+HTTP_X_TENANT
+    *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME
+    identity server-assigned unique identifier, string. For v3 tokens, this
+    will be set to the same value as HTTP_X_PROJECT_ID
+
+HTTP_X_USER
+    *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME
+    User name, unique within owning domain, string
+
+HTTP_X_ROLE
+    *Deprecated* in favor of HTTP_X_ROLES
+    Will contain the same values as HTTP_X_ROLES.
+
+Environment Variables
+^^^^^^^^^^^^^^^^^^^^^
+
+These variables are set in the request environment for use by the downstream
+WSGI component.
+
+keystone.token_info
+    Information about the token discovered in the process of validation.  This
+    may include extended information returned by the token validation call, as
+    well as basic information about the tenant and user.
+
+keystone.token_auth
+    A keystoneclient auth plugin that may be used with a
+    :py:class:`keystoneclient.session.Session`. This plugin will load the
+    authentication data provided to auth_token middleware.
+
+
+Configuration
+-------------
+
+Middleware configuration can be in the main application's configuration file,
+e.g. in ``nova.conf``:
+
+.. code-block:: ini
+
+  [keystone_authtoken]
+  auth_plugin = password
+  auth_url = http://keystone:35357/
+  username = nova
+  user_domain_id = default
+  password = whyarewestillusingpasswords
+  project_name = service
+  project_domain_id = default
+
+Configuration can also be in the ``api-paste.ini`` file with the same options,
+but this is discouraged.
+
+Swift
+-----
+
+When deploy Keystone auth_token middleware with Swift, user may elect to use
+Swift memcache instead of the local auth_token memcache. Swift memcache is
+passed in from the request environment and it's identified by the
+``swift.cache`` key. However it could be different, depending on deployment. To
+use Swift memcache, you must set the ``cache`` option to the environment key
+where the Swift cache object is stored.
+
+"""
+
+import datetime
+import logging
+
+from keystoneclient import access
+from keystoneclient import adapter
+from keystoneclient import auth
+from keystoneclient.common import cms
+from keystoneclient import discover
+from keystoneclient import exceptions
+from keystoneclient import session
+from oslo_config import cfg
+from oslo_serialization import jsonutils
+from oslo_utils import timeutils
+import six
+
+from keystonemiddleware.auth_token import _auth
+from keystonemiddleware.auth_token import _base
+from keystonemiddleware.auth_token import _cache
+from keystonemiddleware.auth_token import _exceptions as exc
+from keystonemiddleware.auth_token import _identity
+from keystonemiddleware.auth_token import _revocations
+from keystonemiddleware.auth_token import _signing_dir
+from keystonemiddleware.auth_token import _user_plugin
+from keystonemiddleware.auth_token import _utils
+from keystonemiddleware.i18n import _, _LC, _LE, _LI, _LW
+
+
+# NOTE(jamielennox): A number of options below are deprecated however are left
+# in the list and only mentioned as deprecated in the help string. This is
+# because we have to provide the same deprecation functionality for arguments
+# passed in via the conf in __init__ (from paste) and there is no way to test
+# that the default value was set or not in CONF.
+# Also if we were to remove the options from the CONF list (as typical CONF
+# deprecation works) then other projects will not be able to override the
+# options via CONF.
+
+_OPTS = [
+    cfg.StrOpt('auth_uri',
+               default=None,
+               # FIXME(dolph): should be default='http://127.0.0.1:5000/v2.0/',
+               # or (depending on client support) an unversioned, publicly
+               # accessible identity endpoint (see bug 1207517)
+               help='Complete public Identity API endpoint.'),
+    cfg.StrOpt('auth_version',
+               default=None,
+               help='API version of the admin Identity API endpoint.'),
+    cfg.BoolOpt('delay_auth_decision',
+                default=False,
+                help='Do not handle authorization requests within the'
+                ' middleware, but delegate the authorization decision to'
+                ' downstream WSGI components.'),
+    cfg.IntOpt('http_connect_timeout',
+               default=None,
+               help='Request timeout value for communicating with Identity'
+               ' API server.'),
+    cfg.IntOpt('http_request_max_retries',
+               default=3,
+               help='How many times are we trying to reconnect when'
+               ' communicating with Identity API Server.'),
+    cfg.StrOpt('cache',
+               default=None,
+               help='Env key for the swift cache.'),
+    cfg.StrOpt('certfile',
+               help='Required if identity server requires client certificate'),
+    cfg.StrOpt('keyfile',
+               help='Required if identity server requires client certificate'),
+    cfg.StrOpt('cafile', default=None,
+               help='A PEM encoded Certificate Authority to use when '
+                    'verifying HTTPs connections. Defaults to system CAs.'),
+    cfg.BoolOpt('insecure', default=False, help='Verify HTTPS connections.'),
+    cfg.StrOpt('signing_dir',
+               help='Directory used to cache files related to PKI tokens.'),
+    cfg.ListOpt('memcached_servers',
+                deprecated_name='memcache_servers',
+                help='Optionally specify a list of memcached server(s) to'
+                ' use for caching. If left undefined, tokens will instead be'
+                ' cached in-process.'),
+    cfg.IntOpt('token_cache_time',
+               default=300,
+               help='In order to prevent excessive effort spent validating'
+               ' tokens, the middleware caches previously-seen tokens for a'
+               ' configurable duration (in seconds). Set to -1 to disable'
+               ' caching completely.'),
+    cfg.IntOpt('revocation_cache_time',
+               default=10,
+               help='Determines the frequency at which the list of revoked'
+               ' tokens is retrieved from the Identity service (in seconds). A'
+               ' high number of revocation events combined with a low cache'
+               ' duration may significantly reduce performance.'),
+    cfg.StrOpt('memcache_security_strategy',
+               default=None,
+               help='(Optional) If defined, indicate whether token data'
+               ' should be authenticated or authenticated and encrypted.'
+               ' Acceptable values are MAC or ENCRYPT.  If MAC, token data is'
+               ' authenticated (with HMAC) in the cache. If ENCRYPT, token'
+               ' data is encrypted and authenticated in the cache. If the'
+               ' value is not one of these options or empty, auth_token will'
+               ' raise an exception on initialization.'),
+    cfg.StrOpt('memcache_secret_key',
+               default=None,
+               secret=True,
+               help='(Optional, mandatory if memcache_security_strategy is'
+               ' defined) This string is used for key derivation.'),
+    cfg.IntOpt('memcache_pool_dead_retry',
+               default=5 * 60,
+               help='(Optional) Number of seconds memcached server is'
+               ' considered dead before it is tried again.'),
+    cfg.IntOpt('memcache_pool_maxsize',
+               default=10,
+               help='(Optional) Maximum total number of open connections to'
+               ' every memcached server.'),
+    cfg.IntOpt('memcache_pool_socket_timeout',
+               default=3,
+               help='(Optional) Socket timeout in seconds for communicating '
+                    'with a memcache server.'),
+    cfg.IntOpt('memcache_pool_unused_timeout',
+               default=60,
+               help='(Optional) Number of seconds a connection to memcached'
+               ' is held unused in the pool before it is closed.'),
+    cfg.IntOpt('memcache_pool_conn_get_timeout',
+               default=10,
+               help='(Optional) Number of seconds that an operation will wait '
+                    'to get a memcache client connection from the pool.'),
+    cfg.BoolOpt('memcache_use_advanced_pool',
+                default=False,
+                help='(Optional) Use the advanced (eventlet safe) memcache '
+                     'client pool. The advanced pool will only work under '
+                     'python 2.x.'),
+    cfg.BoolOpt('include_service_catalog',
+                default=True,
+                help='(Optional) Indicate whether to set the X-Service-Catalog'
+                ' header. If False, middleware will not ask for service'
+                ' catalog on token validation and will not set the'
+                ' X-Service-Catalog header.'),
+    cfg.StrOpt('enforce_token_bind',
+               default='permissive',
+               help='Used to control the use and type of token binding. Can'
+               ' be set to: "disabled" to not check token binding.'
+               ' "permissive" (default) to validate binding information if the'
+               ' bind type is of a form known to the server and ignore it if'
+               ' not. "strict" like "permissive" but if the bind type is'
+               ' unknown the token will be rejected. "required" any form of'
+               ' token binding is needed to be allowed. Finally the name of a'
+               ' binding method that must be present in tokens.'),
+    cfg.BoolOpt('check_revocations_for_cached', default=False,
+                help='If true, the revocation list will be checked for cached'
+                ' tokens. This requires that PKI tokens are configured on the'
+                ' identity server.'),
+    cfg.ListOpt('hash_algorithms', default=['md5'],
+                help='Hash algorithms to use for hashing PKI tokens. This may'
+                ' be a single algorithm or multiple. The algorithms are those'
+                ' supported by Python standard hashlib.new(). The hashes will'
+                ' be tried in the order given, so put the preferred one first'
+                ' for performance. The result of the first hash will be stored'
+                ' in the cache. This will typically be set to multiple values'
+                ' only while migrating from a less secure algorithm to a more'
+                ' secure one. Once all the old tokens are expired this option'
+                ' should be set to a single value for better performance.'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(_OPTS, group=_base.AUTHTOKEN_GROUP)
+
+_LOG = logging.getLogger(__name__)
+
+_HEADER_TEMPLATE = {
+    'X%s-Domain-Id': 'domain_id',
+    'X%s-Domain-Name': 'domain_name',
+    'X%s-Project-Id': 'project_id',
+    'X%s-Project-Name': 'project_name',
+    'X%s-Project-Domain-Id': 'project_domain_id',
+    'X%s-Project-Domain-Name': 'project_domain_name',
+    'X%s-User-Id': 'user_id',
+    'X%s-User-Name': 'username',
+    'X%s-User-Domain-Id': 'user_domain_id',
+    'X%s-User-Domain-Name': 'user_domain_name',
+}
+
+_DEPRECATED_HEADER_TEMPLATE = {
+    'X-User': 'username',
+    'X-Tenant-Id': 'project_id',
+    'X-Tenant-Name': 'project_name',
+    'X-Tenant': 'project_name',
+}
+
+
+class _BIND_MODE(object):
+    DISABLED = 'disabled'
+    PERMISSIVE = 'permissive'
+    STRICT = 'strict'
+    REQUIRED = 'required'
+    KERBEROS = 'kerberos'
+
+
+def _token_is_v2(token_info):
+    return ('access' in token_info)
+
+
+def _token_is_v3(token_info):
+    return ('token' in token_info)
+
+
+def _get_token_expiration(data):
+    if not data:
+        raise exc.InvalidToken(_('Token authorization failed'))
+    if _token_is_v2(data):
+        return data['access']['token']['expires']
+    elif _token_is_v3(data):
+        return data['token']['expires_at']
+    else:
+        raise exc.InvalidToken(_('Token authorization failed'))
+
+
+def _confirm_token_not_expired(expires):
+    expires = timeutils.parse_isotime(expires)
+    expires = timeutils.normalize_time(expires)
+    utcnow = timeutils.utcnow()
+    if utcnow >= expires:
+        raise exc.InvalidToken(_('Token authorization failed'))
+
+
+def _v3_to_v2_catalog(catalog):
+    """Convert a catalog to v2 format.
+
+    X_SERVICE_CATALOG must be specified in v2 format. If you get a token
+    that is in v3 convert it.
+    """
+    v2_services = []
+    for v3_service in catalog:
+        # first copy over the entries we allow for the service
+        v2_service = {'type': v3_service['type']}
+        try:
+            v2_service['name'] = v3_service['name']
+        except KeyError:
+            pass
+
+        # now convert the endpoints. Because in v3 we specify region per
+        # URL not per group we have to collect all the entries of the same
+        # region together before adding it to the new service.
+        regions = {}
+        for v3_endpoint in v3_service.get('endpoints', []):
+            region_name = v3_endpoint.get('region')
+            try:
+                region = regions[region_name]
+            except KeyError:
+                region = {'region': region_name} if region_name else {}
+                regions[region_name] = region
+
+            interface_name = v3_endpoint['interface'].lower() + 'URL'
+            region[interface_name] = v3_endpoint['url']
+
+        v2_service['endpoints'] = list(regions.values())
+        v2_services.append(v2_service)
+
+    return v2_services
+
+
+def _conf_values_type_convert(conf):
+    """Convert conf values into correct type."""
+    if not conf:
+        return {}
+
+    opt_types = {}
+    for o in (_OPTS + _auth.AuthTokenPlugin.get_options()):
+        type_dest = (getattr(o, 'type', str), o.dest)
+        opt_types[o.dest] = type_dest
+        # Also add the deprecated name with the same type and dest.
+        for d_o in o.deprecated_opts:
+            opt_types[d_o.name] = type_dest
+
+    opts = {}
+    for k, v in six.iteritems(conf):
+        dest = k
+        try:
+            if v is not None:
+                type_, dest = opt_types[k]
+                v = type_(v)
+        except KeyError:
+            # This option is not known to auth_token.
+            pass
+        except ValueError as e:
+            raise exc.ConfigurationError(
+                _('Unable to convert the value of %(key)s option into correct '
+                  'type: %(ex)s') % {'key': k, 'ex': e})
+        opts[dest] = v
+    return opts
+
+
+class AuthProtocol(object):
+    """Middleware that handles authenticating client calls."""
+
+    _SIGNING_CERT_FILE_NAME = 'signing_cert.pem'
+    _SIGNING_CA_FILE_NAME = 'cacert.pem'
+
+    def __init__(self, app, conf):
+        self._LOG = logging.getLogger(conf.get('log_name', __name__))
+        self._LOG.info(_LI('Starting Keystone auth_token middleware'))
+        # NOTE(wanghong): If options are set in paste file, all the option
+        # values passed into conf are string type. So, we should convert the
+        # conf value into correct type.
+        self._conf = _conf_values_type_convert(conf)
+        self._app = app
+
+        # delay_auth_decision means we still allow unauthenticated requests
+        # through and we let the downstream service make the final decision
+        self._delay_auth_decision = self._conf_get('delay_auth_decision')
+        self._include_service_catalog = self._conf_get(
+            'include_service_catalog')
+
+        self._identity_server = self._create_identity_server()
+
+        self._auth_uri = self._conf_get('auth_uri')
+        if not self._auth_uri:
+            self._LOG.warning(
+                _LW('Configuring auth_uri to point to the public identity '
+                    'endpoint is required; clients may not be able to '
+                    'authenticate against an admin endpoint'))
+
+            # FIXME(dolph): drop support for this fallback behavior as
+            # documented in bug 1207517.
+
+            self._auth_uri = self._identity_server.auth_uri
+
+        self._signing_directory = _signing_dir.SigningDirectory(
+            directory_name=self._conf_get('signing_dir'), log=self._LOG)
+
+        self._token_cache = self._token_cache_factory()
+
+        revocation_cache_timeout = datetime.timedelta(
+            seconds=self._conf_get('revocation_cache_time'))
+        self._revocations = _revocations.Revocations(revocation_cache_timeout,
+                                                     self._signing_directory,
+                                                     self._identity_server,
+                                                     self._cms_verify,
+                                                     self._LOG)
+
+        self._check_revocations_for_cached = self._conf_get(
+            'check_revocations_for_cached')
+        self._init_auth_headers()
+
+    def _conf_get(self, name, group=_base.AUTHTOKEN_GROUP):
+        # try config from paste-deploy first
+        if name in self._conf:
+            return self._conf[name]
+        else:
+            return CONF[group][name]
+
+    def _call_app(self, env, start_response):
+        # NOTE(jamielennox): We wrap the given start response so that if an
+        # application with a 'delay_auth_decision' setting fails, or otherwise
+        # raises Unauthorized that we include the Authentication URL headers.
+        def _fake_start_response(status, response_headers, exc_info=None):
+            if status.startswith('401'):
+                response_headers.extend(self._reject_auth_headers)
+
+            return start_response(status, response_headers, exc_info)
+
+        return self._app(env, _fake_start_response)
+
+    def __call__(self, env, start_response):
+        """Handle incoming request.
+
+        Authenticate send downstream on success. Reject request if
+        we can't authenticate.
+
+        """
+        def _fmt_msg(env):
+            msg = ('user: user_id %s, project_id %s, roles %s '
+                   'service: user_id %s, project_id %s, roles %s' % (
+                       env.get('HTTP_X_USER_ID'), env.get('HTTP_X_PROJECT_ID'),
+                       env.get('HTTP_X_ROLES'),
+                       env.get('HTTP_X_SERVICE_USER_ID'),
+                       env.get('HTTP_X_SERVICE_PROJECT_ID'),
+                       env.get('HTTP_X_SERVICE_ROLES')))
+            return msg
+
+        self._token_cache.initialize(env)
+        self._remove_auth_headers(env)
+
+        try:
+            user_auth_ref = None
+            serv_auth_ref = None
+
+            try:
+                self._LOG.debug('Authenticating user token')
+                user_token = self._get_user_token_from_header(env)
+                user_token_info = self._validate_token(user_token, env)
+                user_auth_ref = access.AccessInfo.factory(
+                    body=user_token_info,
+                    auth_token=user_token)
+                env['keystone.token_info'] = user_token_info
+                user_headers = self._build_user_headers(user_auth_ref,
+                                                        user_token_info)
+                self._add_headers(env, user_headers)
+            except exc.InvalidToken:
+                if self._delay_auth_decision:
+                    self._LOG.info(
+                        _LI('Invalid user token - deferring reject '
+                            'downstream'))
+                    self._add_headers(env, {'X-Identity-Status': 'Invalid'})
+                else:
+                    self._LOG.info(
+                        _LI('Invalid user token - rejecting request'))
+                    return self._reject_request(env, start_response)
+
+            try:
+                self._LOG.debug('Authenticating service token')
+                serv_token = self._get_service_token_from_header(env)
+                if serv_token is not None:
+                    serv_token_info = self._validate_token(
+                        serv_token, env)
+                    serv_auth_ref = access.AccessInfo.factory(
+                        body=serv_token_info,
+                        auth_token=serv_token)
+                    serv_headers = self._build_service_headers(serv_token_info)
+                    self._add_headers(env, serv_headers)
+            except exc.InvalidToken:
+                if self._delay_auth_decision:
+                    self._LOG.info(
+                        _LI('Invalid service token - deferring reject '
+                            'downstream'))
+                    self._add_headers(env,
+                                      {'X-Service-Identity-Status': 'Invalid'})
+                else:
+                    self._LOG.info(
+                        _LI('Invalid service token - rejecting request'))
+                    return self._reject_request(env, start_response)
+
+            env['keystone.token_auth'] = _user_plugin.UserAuthPlugin(
+                user_auth_ref, serv_auth_ref)
+
+        except exc.ServiceError as e:
+            self._LOG.critical(_LC('Unable to obtain admin token: %s'), e)
+            return self._do_503_error(env, start_response)
+
+        self._LOG.debug("Received request from %s", _fmt_msg(env))
+
+        return self._call_app(env, start_response)
+
+    def _do_503_error(self, env, start_response):
+        resp = _utils.MiniResp('Service unavailable', env)
+        start_response('503 Service Unavailable', resp.headers)
+        return resp.body
+
+    def _init_auth_headers(self):
+        """Initialize auth header list.
+
+        Both user and service token headers are generated.
+        """
+        auth_headers = ['X-Service-Catalog',
+                        'X-Identity-Status',
+                        'X-Service-Identity-Status',
+                        'X-Roles',
+                        'X-Service-Roles']
+        for key in six.iterkeys(_HEADER_TEMPLATE):
+            auth_headers.append(key % '')
+            # Service headers
+            auth_headers.append(key % '-Service')
+
+        # Deprecated headers
+        auth_headers.append('X-Role')
+        for key in six.iterkeys(_DEPRECATED_HEADER_TEMPLATE):
+            auth_headers.append(key)
+
+        self._auth_headers = auth_headers
+
+    def _remove_auth_headers(self, env):
+        """Remove headers so a user can't fake authentication.
+
+        Both user and service token headers are removed.
+
+        :param env: wsgi request environment
+
+        """
+        self._LOG.debug('Removing headers from request environment: %s',
+                        ','.join(self._auth_headers))
+        self._remove_headers(env, self._auth_headers)
+
+    def _get_user_token_from_header(self, env):
+        """Get token id from request.
+
+        :param env: wsgi request environment
+        :returns: token id
+        :raises exc.InvalidToken: if no token is provided in request
+
+        """
+        token = self._get_header(env, 'X-Auth-Token',
+                                 self._get_header(env, 'X-Storage-Token'))
+        if token:
+            return token
+        else:
+            if not self._delay_auth_decision:
+                self._LOG.warn(_LW('Unable to find authentication token'
+                                   ' in headers'))
+                self._LOG.debug('Headers: %s', env)
+            raise exc.InvalidToken(_('Unable to find token in headers'))
+
+    def _get_service_token_from_header(self, env):
+        """Get service token id from request.
+
+        :param env: wsgi request environment
+        :returns: service token id or None if not present
+
+        """
+        return self._get_header(env, 'X-Service-Token')
+
+    @property
+    def _reject_auth_headers(self):
+        header_val = 'Keystone uri=\'%s\'' % self._auth_uri
+        return [('WWW-Authenticate', header_val)]
+
+    def _reject_request(self, env, start_response):
+        """Redirect client to auth server.
+
+        :param env: wsgi request environment
+        :param start_response: wsgi response callback
+        :returns: HTTPUnauthorized http response
+
+        """
+        resp = _utils.MiniResp('Authentication required',
+                               env, self._reject_auth_headers)
+        start_response('401 Unauthorized', resp.headers)
+        return resp.body
+
+    def _validate_token(self, token, env, retry=True):
+        """Authenticate user token
+
+        :param token: token id
+        :param env: wsgi environment
+        :param retry: Ignored, as it is not longer relevant
+        :returns: uncrypted body of the token if the token is valid
+        :raises exc.InvalidToken: if token is rejected
+
+        """
+        token_id = None
+
+        try:
+            token_ids, cached = self._token_cache.get(token)
+            token_id = token_ids[0]
+            if cached:
+                # Token was retrieved from the cache. In this case, there's no
+                # need to check that the token is expired because the cache
+                # fetch fails for an expired token. Also, there's no need to
+                # put the token in the cache because it's already in the cache.
+
+                data = cached
+
+                if self._check_revocations_for_cached:
+                    # A token stored in Memcached might have been revoked
+                    # regardless of initial mechanism used to validate it,
+                    # and needs to be checked.
+                    self._revocations.check(token_ids)
+                self._confirm_token_bind(data, env)
+            else:
+                verified = None
+                # Token wasn't cached. In this case, the token needs to be
+                # checked that it's not expired, and also put in the cache.
+                try:
+                    if cms.is_pkiz(token):
+                        verified = self._verify_pkiz_token(token, token_ids)
+                    elif cms.is_asn1_token(token):
+                        verified = self._verify_signed_token(token, token_ids)
+                except exceptions.CertificateConfigError:
+                    self._LOG.warn(_LW('Fetch certificate config failed, '
+                                       'fallback to online validation.'))
+                except exc.RevocationListError:
+                    self._LOG.warn(_LW('Fetch revocation list failed, '
+                                       'fallback to online validation.'))
+
+                if verified is not None:
+                    data = jsonutils.loads(verified)
+                    expires = _get_token_expiration(data)
+                    _confirm_token_not_expired(expires)
+                else:
+                    data = self._identity_server.verify_token(token, retry)
+                    # No need to confirm token expiration here since
+                    # verify_token fails for expired tokens.
+                    expires = _get_token_expiration(data)
+                self._confirm_token_bind(data, env)
+                self._token_cache.store(token_id, data, expires)
+            return data
+        except (exceptions.ConnectionRefused, exceptions.RequestTimeout):
+            self._LOG.debug('Token validation failure.', exc_info=True)
+            self._LOG.warn(_LW('Authorization failed for token'))
+            raise exc.InvalidToken(_('Token authorization failed'))
+        except exc.ServiceError:
+            raise
+        except Exception:
+            self._LOG.debug('Token validation failure.', exc_info=True)
+            if token_id:
+                self._token_cache.store_invalid(token_id)
+            self._LOG.warn(_LW('Authorization failed for token'))
+            raise exc.InvalidToken(_('Token authorization failed'))
+
+    def _build_user_headers(self, auth_ref, token_info):
+        """Convert token object into headers.
+
+        Build headers that represent authenticated user - see main
+        doc info at start of file for details of headers to be defined.
+
+        :param token_info: token object returned by identity
+                           server on authentication
+        :raises exc.InvalidToken: when unable to parse token object
+
+        """
+        roles = ','.join(auth_ref.role_names)
+
+        if _token_is_v2(token_info) and not auth_ref.project_id:
+            raise exc.InvalidToken(_('Unable to determine tenancy.'))
+
+        rval = {
+            'X-Identity-Status': 'Confirmed',
+            'X-Roles': roles,
+        }
+
+        for header_tmplt, attr in six.iteritems(_HEADER_TEMPLATE):
+            rval[header_tmplt % ''] = getattr(auth_ref, attr)
+
+        # Deprecated headers
+        rval['X-Role'] = roles
+        for header_tmplt, attr in six.iteritems(_DEPRECATED_HEADER_TEMPLATE):
+            rval[header_tmplt] = getattr(auth_ref, attr)
+
+        if self._include_service_catalog and auth_ref.has_service_catalog():
+            catalog = auth_ref.service_catalog.get_data()
+            if _token_is_v3(token_info):
+                catalog = _v3_to_v2_catalog(catalog)
+            rval['X-Service-Catalog'] = jsonutils.dumps(catalog)
+
+        return rval
+
+    def _build_service_headers(self, token_info):
+        """Convert token object into service headers.
+
+        Build headers that represent authenticated user - see main
+        doc info at start of file for details of headers to be defined.
+
+        :param token_info: token object returned by identity
+                           server on authentication
+        :raises exc.InvalidToken: when unable to parse token object
+
+        """
+        auth_ref = access.AccessInfo.factory(body=token_info)
+
+        if _token_is_v2(token_info) and not auth_ref.project_id:
+            raise exc.InvalidToken(_('Unable to determine service tenancy.'))
+
+        roles = ','.join(auth_ref.role_names)
+        rval = {
+            'X-Service-Identity-Status': 'Confirmed',
+            'X-Service-Roles': roles,
+        }
+
+        header_type = '-Service'
+        for header_tmplt, attr in six.iteritems(_HEADER_TEMPLATE):
+            rval[header_tmplt % header_type] = getattr(auth_ref, attr)
+
+        return rval
+
+    def _header_to_env_var(self, key):
+        """Convert header to wsgi env variable.
+
+        :param key: http header name (ex. 'X-Auth-Token')
+        :returns: wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN')
+
+        """
+        return 'HTTP_%s' % key.replace('-', '_').upper()
+
+    def _add_headers(self, env, headers):
+        """Add http headers to environment."""
+        for (k, v) in six.iteritems(headers):
+            env_key = self._header_to_env_var(k)
+            env[env_key] = v
+
+    def _remove_headers(self, env, keys):
+        """Remove http headers from environment."""
+        for k in keys:
+            env_key = self._header_to_env_var(k)
+            try:
+                del env[env_key]
+            except KeyError:
+                pass
+
+    def _get_header(self, env, key, default=None):
+        """Get http header from environment."""
+        env_key = self._header_to_env_var(key)
+        return env.get(env_key, default)
+
+    def _invalid_user_token(self, msg=False):
+        # NOTE(jamielennox): use False as the default so that None is valid
+        if msg is False:
+            msg = _('Token authorization failed')
+
+        raise exc.InvalidToken(msg)
+
+    def _confirm_token_bind(self, data, env):
+        bind_mode = self._conf_get('enforce_token_bind')
+
+        if bind_mode == _BIND_MODE.DISABLED:
+            return
+
+        try:
+            if _token_is_v2(data):
+                bind = data['access']['token']['bind']
+            elif _token_is_v3(data):
+                bind = data['token']['bind']
+            else:
+                self._invalid_user_token()
+        except KeyError:
+            bind = {}
+
+        # permissive and strict modes don't require there to be a bind
+        permissive = bind_mode in (_BIND_MODE.PERMISSIVE, _BIND_MODE.STRICT)
+
+        if not bind:
+            if permissive:
+                # no bind provided and none required
+                return
+            else:
+                self._LOG.info(_LI('No bind information present in token.'))
+                self._invalid_user_token()
+
+        # get the named mode if bind_mode is not one of the predefined
+        if permissive or bind_mode == _BIND_MODE.REQUIRED:
+            name = None
+        else:
+            name = bind_mode
+
+        if name and name not in bind:
+            self._LOG.info(_LI('Named bind mode %s not in bind information'),
+                           name)
+            self._invalid_user_token()
+
+        for bind_type, identifier in six.iteritems(bind):
+            if bind_type == _BIND_MODE.KERBEROS:
+                if not env.get('AUTH_TYPE', '').lower() == 'negotiate':
+                    self._LOG.info(_LI('Kerberos credentials required and '
+                                       'not present.'))
+                    self._invalid_user_token()
+
+                if not env.get('REMOTE_USER') == identifier:
+                    self._LOG.info(_LI('Kerberos credentials do not match '
+                                       'those in bind.'))
+                    self._invalid_user_token()
+
+                self._LOG.debug('Kerberos bind authentication successful.')
+
+            elif bind_mode == _BIND_MODE.PERMISSIVE:
+                self._LOG.debug('Ignoring Unknown bind for permissive mode: '
+                                '%(bind_type)s: %(identifier)s.',
+                                {'bind_type': bind_type,
+                                 'identifier': identifier})
+
+            else:
+                self._LOG.info(
+                    _LI('Couldn`t verify unknown bind: %(bind_type)s: '
+                        '%(identifier)s.'),
+                    {'bind_type': bind_type, 'identifier': identifier})
+                self._invalid_user_token()
+
+    def _cms_verify(self, data, inform=cms.PKI_ASN1_FORM):
+        """Verifies the signature of the provided data's IAW CMS syntax.
+
+        If either of the certificate files might be missing, fetch them and
+        retry.
+        """
+        def verify():
+            try:
+                signing_cert_path = self._signing_directory.calc_path(
+                    self._SIGNING_CERT_FILE_NAME)
+                signing_ca_path = self._signing_directory.calc_path(
+                    self._SIGNING_CA_FILE_NAME)
+                return cms.cms_verify(data, signing_cert_path,
+                                      signing_ca_path,
+                                      inform=inform).decode('utf-8')
+            except cms.subprocess.CalledProcessError as err:
+                self._LOG.warning(_LW('Verify error: %s'), err)
+                raise
+
+        try:
+            return verify()
+        except exceptions.CertificateConfigError:
+            # the certs might be missing; unconditionally fetch to avoid racing
+            self._fetch_signing_cert()
+            self._fetch_ca_cert()
+
+            try:
+                # retry with certs in place
+                return verify()
+            except exceptions.CertificateConfigError as err:
+                # if this is still occurring, something else is wrong and we
+                # need err.output to identify the problem
+                self._LOG.error(_LE('CMS Verify output: %s'), err.output)
+                raise
+
+    def _verify_signed_token(self, signed_text, token_ids):
+        """Check that the token is unrevoked and has a valid signature."""
+        self._revocations.check(token_ids)
+        formatted = cms.token_to_cms(signed_text)
+        verified = self._cms_verify(formatted)
+        return verified
+
+    def _verify_pkiz_token(self, signed_text, token_ids):
+        self._revocations.check(token_ids)
+        try:
+            uncompressed = cms.pkiz_uncompress(signed_text)
+            verified = self._cms_verify(uncompressed, inform=cms.PKIZ_CMS_FORM)
+            return verified
+        # TypeError If the signed_text is not zlib compressed
+        except TypeError:
+            raise exc.InvalidToken(signed_text)
+
+    def _fetch_signing_cert(self):
+        self._signing_directory.write_file(
+            self._SIGNING_CERT_FILE_NAME,
+            self._identity_server.fetch_signing_cert())
+
+    def _fetch_ca_cert(self):
+        self._signing_directory.write_file(
+            self._SIGNING_CA_FILE_NAME,
+            self._identity_server.fetch_ca_cert())
+
+    def _get_auth_plugin(self):
+        # NOTE(jamielennox): Ideally this would use get_from_conf_options
+        # however that is not possible because we have to support the override
+        # pattern we use in _conf_get. There is a somewhat replacement for this
+        # in keystoneclient in load_from_options_getter which should be used
+        # when available. Until then this is essentially a copy and paste of
+        # the ksc load_from_conf_options code because we need to get a fix out
+        # for this quickly.
+
+        # FIXME(jamielennox): update to use load_from_options_getter when
+        # https://review.openstack.org/162529 merges.
+
+        # !!! - UNDER NO CIRCUMSTANCES COPY ANY OF THIS CODE - !!!
+
+        group = self._conf_get('auth_section') or _base.AUTHTOKEN_GROUP
+        plugin_name = self._conf_get('auth_plugin', group=group)
+        plugin_kwargs = dict()
+
+        if plugin_name:
+            plugin_class = auth.get_plugin_class(plugin_name)
+        else:
+            plugin_class = _auth.AuthTokenPlugin
+            # logger object is a required parameter of the default plugin
+            plugin_kwargs['log'] = self._LOG
+
+        plugin_opts = plugin_class.get_options()
+        CONF.register_opts(plugin_opts, group=group)
+
+        for opt in plugin_opts:
+            val = self._conf_get(opt.dest, group=group)
+            if val is not None:
+                val = opt.type(val)
+            plugin_kwargs[opt.dest] = val
+
+        return plugin_class.load_from_options(**plugin_kwargs)
+
+    def _create_identity_server(self):
+        # NOTE(jamielennox): Loading Session here should be exactly the
+        # same as calling Session.load_from_conf_options(CONF, GROUP)
+        # however we can't do that because we have to use _conf_get to
+        # support the paste.ini options.
+        sess = session.Session.construct(dict(
+            cert=self._conf_get('certfile'),
+            key=self._conf_get('keyfile'),
+            cacert=self._conf_get('cafile'),
+            insecure=self._conf_get('insecure'),
+            timeout=self._conf_get('http_connect_timeout')
+        ))
+
+        auth_plugin = self._get_auth_plugin()
+
+        adap = adapter.Adapter(
+            sess,
+            auth=auth_plugin,
+            service_type='identity',
+            interface='admin',
+            connect_retries=self._conf_get('http_request_max_retries'))
+
+        auth_version = self._conf_get('auth_version')
+        if auth_version is not None:
+            auth_version = discover.normalize_version_number(auth_version)
+        return _identity.IdentityServer(
+            self._LOG,
+            adap,
+            include_service_catalog=self._include_service_catalog,
+            requested_auth_version=auth_version)
+
+    def _token_cache_factory(self):
+        security_strategy = self._conf_get('memcache_security_strategy')
+
+        cache_kwargs = dict(
+            cache_time=int(self._conf_get('token_cache_time')),
+            hash_algorithms=self._conf_get('hash_algorithms'),
+            env_cache_name=self._conf_get('cache'),
+            memcached_servers=self._conf_get('memcached_servers'),
+            use_advanced_pool=self._conf_get('memcache_use_advanced_pool'),
+            memcache_pool_dead_retry=self._conf_get(
+                'memcache_pool_dead_retry'),
+            memcache_pool_maxsize=self._conf_get('memcache_pool_maxsize'),
+            memcache_pool_unused_timeout=self._conf_get(
+                'memcache_pool_unused_timeout'),
+            memcache_pool_conn_get_timeout=self._conf_get(
+                'memcache_pool_conn_get_timeout'),
+            memcache_pool_socket_timeout=self._conf_get(
+                'memcache_pool_socket_timeout'),
+        )
+
+        if security_strategy:
+            secret_key = self._conf_get('memcache_secret_key')
+            return _cache.SecureTokenCache(self._LOG,
+                                           security_strategy,
+                                           secret_key,
+                                           **cache_kwargs)
+        else:
+            return _cache.TokenCache(self._LOG, **cache_kwargs)
+
+
+def filter_factory(global_conf, **local_conf):
+    """Returns a WSGI filter app for use with paste.deploy."""
+    conf = global_conf.copy()
+    conf.update(local_conf)
+
+    def auth_filter(app):
+        return AuthProtocol(app, conf)
+    return auth_filter
+
+
+def app_factory(global_conf, **local_conf):
+    conf = global_conf.copy()
+    conf.update(local_conf)
+    return AuthProtocol(None, conf)
+
+
+if __name__ == '__main__':
+    def echo_app(environ, start_response):
+        """A WSGI application that echoes the CGI environment to the user."""
+        start_response('200 OK', [('Content-Type', 'application/json')])
+        environment = dict((k, v) for k, v in six.iteritems(environ)
+                           if k.startswith('HTTP_X_'))
+        yield jsonutils.dumps(environment)
+
+    from wsgiref import simple_server
+
+    # hardcode any non-default configuration here
+    conf = {'auth_protocol': 'http', 'admin_token': 'ADMIN'}
+    app = AuthProtocol(echo_app, conf)
+    server = simple_server.make_server('', 8000, app)
+    print('Serving on port 8000 (Ctrl+C to end)...')
+    server.serve_forever()
+
+
+# NOTE(jamielennox): Maintained here for public API compatibility.
+InvalidToken = exc.InvalidToken
+ServiceError = exc.ServiceError
+ConfigurationError = exc.ConfigurationError
+RevocationListError = exc.RevocationListError
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_auth.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_auth.py
new file mode 100644 (file)
index 0000000..acc32ca
--- /dev/null
@@ -0,0 +1,181 @@
+# 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.
+
+import logging
+
+from keystoneclient import auth
+from keystoneclient.auth.identity import v2
+from keystoneclient.auth import token_endpoint
+from keystoneclient import discover
+from oslo_config import cfg
+
+from keystonemiddleware.auth_token import _base
+from keystonemiddleware.i18n import _, _LW
+
+
+_LOG = logging.getLogger(__name__)
+
+
+class AuthTokenPlugin(auth.BaseAuthPlugin):
+
+    def __init__(self, auth_host, auth_port, auth_protocol, auth_admin_prefix,
+                 admin_user, admin_password, admin_tenant_name, admin_token,
+                 identity_uri, log):
+        # NOTE(jamielennox): it does appear here that our default arguments
+        # are backwards. We need to do it this way so that we can handle the
+        # same deprecation strategy for CONF and the conf variable.
+        if not identity_uri:
+            log.warning(_LW('Configuring admin URI using auth fragments. '
+                            'This is deprecated, use \'identity_uri\''
+                            ' instead.'))
+
+            if ':' in auth_host:
+                # Note(dzyu) it is an IPv6 address, so it needs to be wrapped
+                # with '[]' to generate a valid IPv6 URL, based on
+                # http://www.ietf.org/rfc/rfc2732.txt
+                auth_host = '[%s]' % auth_host
+
+            identity_uri = '%s://%s:%s' % (auth_protocol,
+                                           auth_host,
+                                           auth_port)
+
+            if auth_admin_prefix:
+                identity_uri = '%s/%s' % (identity_uri,
+                                          auth_admin_prefix.strip('/'))
+
+        self._identity_uri = identity_uri.rstrip('/')
+
+        # FIXME(jamielennox): Yes. This is wrong. We should be determining the
+        # plugin to use based on a combination of discovery and inputs. Much
+        # of this can be changed when we get keystoneclient 0.10. For now this
+        # hardcoded path is EXACTLY the same as the original auth_token did.
+        auth_url = '%s/v2.0' % self._identity_uri
+
+        if admin_token:
+            log.warning(_LW(
+                "The admin_token option in the auth_token middleware is "
+                "deprecated and should not be used. The admin_user and "
+                "admin_password options should be used instead. The "
+                "admin_token option may be removed in a future release."))
+            self._plugin = token_endpoint.Token(auth_url, admin_token)
+        else:
+            self._plugin = v2.Password(auth_url,
+                                       username=admin_user,
+                                       password=admin_password,
+                                       tenant_name=admin_tenant_name)
+
+        self._LOG = log
+        self._discover = None
+
+    def get_token(self, *args, **kwargs):
+        return self._plugin.get_token(*args, **kwargs)
+
+    def get_endpoint(self, session, interface=None, version=None, **kwargs):
+        """Return an endpoint for the client.
+
+        There are no required keyword arguments to ``get_endpoint`` as a plugin
+        implementation should use best effort with the information available to
+        determine the endpoint.
+
+        :param session: The session object that the auth_plugin belongs to.
+        :type session: keystoneclient.session.Session
+        :param tuple version: The version number required for this endpoint.
+        :param str interface: what visibility the endpoint should have.
+
+        :returns: The base URL that will be used to talk to the required
+                  service or None if not available.
+        :rtype: string
+        """
+        if interface == auth.AUTH_INTERFACE:
+            return self._identity_uri
+
+        if not version:
+            # NOTE(jamielennox): This plugin can only be used within auth_token
+            # and auth_token will always provide version= with requests.
+            return None
+
+        if not self._discover:
+            self._discover = discover.Discover(session,
+                                               auth_url=self._identity_uri,
+                                               authenticated=False)
+
+        if not self._discover.url_for(version):
+            # NOTE(jamielennox): The requested version is not supported by the
+            # identity server.
+            return None
+
+        # NOTE(jamielennox): for backwards compatibility here we don't
+        # actually use the URL from discovery we hack it up instead. :(
+        if version[0] == 2:
+            return '%s/v2.0' % self._identity_uri
+        elif version[0] == 3:
+            return '%s/v3' % self._identity_uri
+
+        # NOTE(jamielennox): This plugin will only get called from auth_token
+        # middleware. The middleware should never request a version that the
+        # plugin doesn't know how to handle.
+        msg = _('Invalid version asked for in auth_token plugin')
+        raise NotImplementedError(msg)
+
+    def invalidate(self):
+        return self._plugin.invalidate()
+
+    @classmethod
+    def get_options(cls):
+        options = super(AuthTokenPlugin, cls).get_options()
+
+        options.extend([
+            cfg.StrOpt('auth_admin_prefix',
+                       default='',
+                       help='Prefix to prepend at the beginning of the path. '
+                            'Deprecated, use identity_uri.'),
+            cfg.StrOpt('auth_host',
+                       default='127.0.0.1',
+                       help='Host providing the admin Identity API endpoint. '
+                            'Deprecated, use identity_uri.'),
+            cfg.IntOpt('auth_port',
+                       default=35357,
+                       help='Port of the admin Identity API endpoint. '
+                            'Deprecated, use identity_uri.'),
+            cfg.StrOpt('auth_protocol',
+                       default='https',
+                       help='Protocol of the admin Identity API endpoint '
+                            '(http or https). Deprecated, use identity_uri.'),
+            cfg.StrOpt('identity_uri',
+                       default=None,
+                       help='Complete admin Identity API endpoint. This '
+                            'should specify the unversioned root endpoint '
+                            'e.g. https://localhost:35357/'),
+            cfg.StrOpt('admin_token',
+                       secret=True,
+                       help='This option is deprecated and may be removed in '
+                            'a future release. Single shared secret with the '
+                            'Keystone configuration used for bootstrapping a '
+                            'Keystone installation, or otherwise bypassing '
+                            'the normal authentication process. This option '
+                            'should not be used, use `admin_user` and '
+                            '`admin_password` instead.'),
+            cfg.StrOpt('admin_user',
+                       help='Service username.'),
+            cfg.StrOpt('admin_password',
+                       secret=True,
+                       help='Service user password.'),
+            cfg.StrOpt('admin_tenant_name',
+                       default='admin',
+                       help='Service tenant name.'),
+        ])
+
+        return options
+
+
+auth.register_conf_options(cfg.CONF, _base.AUTHTOKEN_GROUP)
+AuthTokenPlugin.register_conf_options(cfg.CONF, _base.AUTHTOKEN_GROUP)
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_base.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_base.py
new file mode 100644 (file)
index 0000000..ee4ec13
--- /dev/null
@@ -0,0 +1,13 @@
+# 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.
+
+AUTHTOKEN_GROUP = 'keystone_authtoken'
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_cache.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_cache.py
new file mode 100644 (file)
index 0000000..ae15577
--- /dev/null
@@ -0,0 +1,367 @@
+# 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.
+
+import contextlib
+
+from keystoneclient.common import cms
+from oslo_serialization import jsonutils
+from oslo_utils import timeutils
+import six
+
+from keystonemiddleware.auth_token import _exceptions as exc
+from keystonemiddleware.auth_token import _memcache_crypt as memcache_crypt
+from keystonemiddleware.i18n import _, _LE
+from keystonemiddleware.openstack.common import memorycache
+
+
+class _CachePool(list):
+    """A lazy pool of cache references."""
+
+    def __init__(self, cache, memcached_servers):
+        self._environment_cache = cache
+        self._memcached_servers = memcached_servers
+
+    @contextlib.contextmanager
+    def reserve(self):
+        """Context manager to manage a pooled cache reference."""
+        if self._environment_cache is not None:
+            # skip pooling and just use the cache from the upstream filter
+            yield self._environment_cache
+            return  # otherwise the context manager will continue!
+
+        try:
+            c = self.pop()
+        except IndexError:
+            # the pool is empty, so we need to create a new client
+            c = memorycache.get_client(self._memcached_servers)
+
+        try:
+            yield c
+        finally:
+            self.append(c)
+
+
+class _MemcacheClientPool(object):
+    """An advanced memcached client pool that is eventlet safe."""
+    def __init__(self, memcache_servers, memcache_dead_retry=None,
+                 memcache_pool_maxsize=None, memcache_pool_unused_timeout=None,
+                 memcache_pool_conn_get_timeout=None,
+                 memcache_pool_socket_timeout=None):
+        # NOTE(morganfainberg): import here to avoid hard dependency on
+        # python-memcache library.
+        global _memcache_pool
+        from keystonemiddleware.auth_token import _memcache_pool
+
+        self._pool = _memcache_pool.MemcacheClientPool(
+            memcache_servers,
+            arguments={
+                'dead_retry': memcache_dead_retry,
+                'socket_timeout': memcache_pool_socket_timeout,
+            },
+            maxsize=memcache_pool_maxsize,
+            unused_timeout=memcache_pool_unused_timeout,
+            conn_get_timeout=memcache_pool_conn_get_timeout,
+        )
+
+    @contextlib.contextmanager
+    def reserve(self):
+        with self._pool.get() as client:
+            yield client
+
+
+class TokenCache(object):
+    """Encapsulates the auth_token token cache functionality.
+
+    auth_token caches tokens that it's seen so that when a token is re-used the
+    middleware doesn't have to do a more expensive operation (like going to the
+    identity server) to validate the token.
+
+    initialize() must be called before calling the other methods.
+
+    Store a valid token in the cache using store(); mark a token as invalid in
+    the cache using store_invalid().
+
+    Check if a token is in the cache and retrieve it using get().
+
+    """
+
+    _CACHE_KEY_TEMPLATE = 'tokens/%s'
+    _INVALID_INDICATOR = 'invalid'
+
+    def __init__(self, log, cache_time=None, hash_algorithms=None,
+                 env_cache_name=None, memcached_servers=None,
+                 use_advanced_pool=False, memcache_pool_dead_retry=None,
+                 memcache_pool_maxsize=None, memcache_pool_unused_timeout=None,
+                 memcache_pool_conn_get_timeout=None,
+                 memcache_pool_socket_timeout=None):
+        self._LOG = log
+        self._cache_time = cache_time
+        self._hash_algorithms = hash_algorithms
+        self._env_cache_name = env_cache_name
+        self._memcached_servers = memcached_servers
+        self._use_advanced_pool = use_advanced_pool
+        self._memcache_pool_dead_retry = memcache_pool_dead_retry,
+        self._memcache_pool_maxsize = memcache_pool_maxsize,
+        self._memcache_pool_unused_timeout = memcache_pool_unused_timeout
+        self._memcache_pool_conn_get_timeout = memcache_pool_conn_get_timeout
+        self._memcache_pool_socket_timeout = memcache_pool_socket_timeout
+
+        self._cache_pool = None
+        self._initialized = False
+
+    def _get_cache_pool(self, cache, memcache_servers, use_advanced_pool=False,
+                        memcache_dead_retry=None, memcache_pool_maxsize=None,
+                        memcache_pool_unused_timeout=None,
+                        memcache_pool_conn_get_timeout=None,
+                        memcache_pool_socket_timeout=None):
+        if use_advanced_pool is True and memcache_servers and cache is None:
+            return _MemcacheClientPool(
+                memcache_servers,
+                memcache_dead_retry=memcache_dead_retry,
+                memcache_pool_maxsize=memcache_pool_maxsize,
+                memcache_pool_unused_timeout=memcache_pool_unused_timeout,
+                memcache_pool_conn_get_timeout=memcache_pool_conn_get_timeout,
+                memcache_pool_socket_timeout=memcache_pool_socket_timeout)
+        else:
+            return _CachePool(cache, memcache_servers)
+
+    def initialize(self, env):
+        if self._initialized:
+            return
+
+        self._cache_pool = self._get_cache_pool(
+            env.get(self._env_cache_name),
+            self._memcached_servers,
+            use_advanced_pool=self._use_advanced_pool,
+            memcache_dead_retry=self._memcache_pool_dead_retry,
+            memcache_pool_maxsize=self._memcache_pool_maxsize,
+            memcache_pool_unused_timeout=self._memcache_pool_unused_timeout,
+            memcache_pool_conn_get_timeout=self._memcache_pool_conn_get_timeout
+        )
+
+        self._initialized = True
+
+    def get(self, user_token):
+        """Check if the token is cached already.
+
+        Returns a tuple. The first element is a list of token IDs, where the
+        first one is the preferred hash.
+
+        The second element is the token data from the cache if the token was
+        cached, otherwise ``None``.
+
+        :raises exc.InvalidToken: if the token is invalid
+
+        """
+
+        if cms.is_asn1_token(user_token) or cms.is_pkiz(user_token):
+            # user_token is a PKI token that's not hashed.
+
+            token_hashes = list(cms.cms_hash_token(user_token, mode=algo)
+                                for algo in self._hash_algorithms)
+
+            for token_hash in token_hashes:
+                cached = self._cache_get(token_hash)
+                if cached:
+                    return (token_hashes, cached)
+
+            # The token wasn't found using any hash algorithm.
+            return (token_hashes, None)
+
+        # user_token is either a UUID token or a hashed PKI token.
+        token_id = user_token
+        cached = self._cache_get(token_id)
+        return ([token_id], cached)
+
+    def store(self, token_id, data, expires):
+        """Put token data into the cache.
+
+        Stores the parsed expire date in cache allowing
+        quick check of token freshness on retrieval.
+
+        """
+        self._LOG.debug('Storing token in cache')
+        self._cache_store(token_id, (data, expires))
+
+    def store_invalid(self, token_id):
+        """Store invalid token in cache."""
+        self._LOG.debug('Marking token as unauthorized in cache')
+        self._cache_store(token_id, self._INVALID_INDICATOR)
+
+    def _get_cache_key(self, token_id):
+        """Get a unique key for this token id.
+
+        Turn the token_id into something that can uniquely identify that token
+        in a key value store.
+
+        As this is generally the first function called in a key lookup this
+        function also returns a context object. This context object is not
+        modified or used by the Cache object but is passed back on subsequent
+        functions so that decryption or other data can be shared throughout a
+        cache lookup.
+
+        :param str token_id: The unique token id.
+
+        :returns: A tuple of a string key and an implementation specific
+                  context object
+        """
+        # NOTE(jamielennox): in the basic implementation there is no need for
+        # a context so just pass None as it will only get passed back later.
+        unused_context = None
+        return self._CACHE_KEY_TEMPLATE % token_id, unused_context
+
+    def _deserialize(self, data, context):
+        """Deserialize data from the cache back into python objects.
+
+        Take data retrieved from the cache and return an appropriate python
+        dictionary.
+
+        :param str data: The data retrieved from the cache.
+        :param object context: The context that was returned from
+                               _get_cache_key.
+
+        :returns: The python object that was saved.
+        """
+        # memory cache will handle deserialization for us
+        return data
+
+    def _serialize(self, data, context):
+        """Serialize data so that it can be saved to the cache.
+
+        Take python objects and serialize them so that they can be saved into
+        the cache.
+
+        :param object data: The data to be cached.
+        :param object context: The context that was returned from
+                               _get_cache_key.
+
+        :returns: The python object that was saved.
+        """
+        # memory cache will handle serialization for us
+        return data
+
+    def _cache_get(self, token_id):
+        """Return token information from cache.
+
+        If token is invalid raise exc.InvalidToken
+        return token only if fresh (not expired).
+        """
+
+        if not token_id:
+            # Nothing to do
+            return
+
+        key, context = self._get_cache_key(token_id)
+
+        with self._cache_pool.reserve() as cache:
+            serialized = cache.get(key)
+
+        if serialized is None:
+            return None
+
+        data = self._deserialize(serialized, context)
+
+        # Note that _INVALID_INDICATOR and (data, expires) are the only
+        # valid types of serialized cache entries, so there is not
+        # a collision with jsonutils.loads(serialized) == None.
+        if not isinstance(data, six.string_types):
+            data = data.decode('utf-8')
+        cached = jsonutils.loads(data)
+        if cached == self._INVALID_INDICATOR:
+            self._LOG.debug('Cached Token is marked unauthorized')
+            raise exc.InvalidToken(_('Token authorization failed'))
+
+        data, expires = cached
+
+        try:
+            expires = timeutils.parse_isotime(expires)
+        except ValueError:
+            # Gracefully handle upgrade of expiration times from *nix
+            # timestamps to ISO 8601 formatted dates by ignoring old cached
+            # values.
+            return
+
+        expires = timeutils.normalize_time(expires)
+        utcnow = timeutils.utcnow()
+        if utcnow < expires:
+            self._LOG.debug('Returning cached token')
+            return data
+        else:
+            self._LOG.debug('Cached Token seems expired')
+            raise exc.InvalidToken(_('Token authorization failed'))
+
+    def _cache_store(self, token_id, data):
+        """Store value into memcache.
+
+        data may be _INVALID_INDICATOR or a tuple like (data, expires)
+
+        """
+        data = jsonutils.dumps(data)
+        if isinstance(data, six.text_type):
+            data = data.encode('utf-8')
+
+        cache_key, context = self._get_cache_key(token_id)
+        data_to_store = self._serialize(data, context)
+
+        with self._cache_pool.reserve() as cache:
+            cache.set(cache_key, data_to_store, time=self._cache_time)
+
+
+class SecureTokenCache(TokenCache):
+    """A token cache that stores tokens encrypted.
+
+    A more secure version of TokenCache that will encrypt tokens before
+    caching them.
+    """
+
+    def __init__(self, log, security_strategy, secret_key, **kwargs):
+        super(SecureTokenCache, self).__init__(log, **kwargs)
+
+        security_strategy = security_strategy.upper()
+
+        if security_strategy not in ('MAC', 'ENCRYPT'):
+            msg = _('memcache_security_strategy must be ENCRYPT or MAC')
+            raise exc.ConfigurationError(msg)
+        if not secret_key:
+            msg = _('memcache_secret_key must be defined when a '
+                    'memcache_security_strategy is defined')
+            raise exc.ConfigurationError(msg)
+
+        if isinstance(security_strategy, six.string_types):
+            security_strategy = security_strategy.encode('utf-8')
+        if isinstance(secret_key, six.string_types):
+            secret_key = secret_key.encode('utf-8')
+
+        self._security_strategy = security_strategy
+        self._secret_key = secret_key
+
+    def _get_cache_key(self, token_id):
+        context = memcache_crypt.derive_keys(token_id,
+                                             self._secret_key,
+                                             self._security_strategy)
+        key = self._CACHE_KEY_TEMPLATE % memcache_crypt.get_cache_key(context)
+        return key, context
+
+    def _deserialize(self, data, context):
+        try:
+            # unprotect_data will return None if raw_cached is None
+            return memcache_crypt.unprotect_data(context, data)
+        except Exception:
+            msg = _LE('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 _serialize(self, data, context):
+        return memcache_crypt.protect_data(context, data)
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_exceptions.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_exceptions.py
new file mode 100644 (file)
index 0000000..be045c9
--- /dev/null
@@ -0,0 +1,27 @@
+# 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.
+
+
+class InvalidToken(Exception):
+    pass
+
+
+class ServiceError(Exception):
+    pass
+
+
+class ConfigurationError(Exception):
+    pass
+
+
+class RevocationListError(Exception):
+    pass
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_identity.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_identity.py
new file mode 100644 (file)
index 0000000..8acf70d
--- /dev/null
@@ -0,0 +1,243 @@
+# 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.
+
+from keystoneclient import auth
+from keystoneclient import discover
+from keystoneclient import exceptions
+from oslo_serialization import jsonutils
+from six.moves import urllib
+
+from keystonemiddleware.auth_token import _auth
+from keystonemiddleware.auth_token import _exceptions as exc
+from keystonemiddleware.auth_token import _utils
+from keystonemiddleware.i18n import _, _LE, _LI, _LW
+
+
+class _RequestStrategy(object):
+
+    AUTH_VERSION = None
+
+    def __init__(self, json_request, adap, include_service_catalog=None):
+        self._json_request = json_request
+        self._adapter = adap
+        self._include_service_catalog = include_service_catalog
+
+    def verify_token(self, user_token):
+        pass
+
+    def fetch_cert_file(self, cert_type):
+        pass
+
+
+class _V2RequestStrategy(_RequestStrategy):
+
+    AUTH_VERSION = (2, 0)
+
+    def verify_token(self, user_token):
+        return self._json_request('GET',
+                                  '/tokens/%s' % user_token,
+                                  authenticated=True)
+
+    def fetch_cert_file(self, cert_type):
+        return self._adapter.get('/certificates/%s' % cert_type,
+                                 authenticated=False)
+
+
+class _V3RequestStrategy(_RequestStrategy):
+
+    AUTH_VERSION = (3, 0)
+
+    def verify_token(self, user_token):
+        path = '/auth/tokens'
+        if not self._include_service_catalog:
+            path += '?nocatalog'
+
+        return self._json_request('GET',
+                                  path,
+                                  authenticated=True,
+                                  headers={'X-Subject-Token': user_token})
+
+    def fetch_cert_file(self, cert_type):
+        if cert_type == 'signing':
+            cert_type = 'certificates'
+
+        return self._adapter.get('/OS-SIMPLE-CERT/%s' % cert_type,
+                                 authenticated=False)
+
+
+_REQUEST_STRATEGIES = [_V3RequestStrategy, _V2RequestStrategy]
+
+
+class IdentityServer(object):
+    """Base class for operations on the Identity API server.
+
+    The auth_token middleware needs to communicate with the Identity API server
+    to validate UUID tokens, fetch the revocation list, signing certificates,
+    etc. This class encapsulates the data and methods to perform these
+    operations.
+
+    """
+
+    def __init__(self, log, adap, include_service_catalog=None,
+                 requested_auth_version=None):
+        self._LOG = log
+        self._adapter = adap
+        self._include_service_catalog = include_service_catalog
+        self._requested_auth_version = requested_auth_version
+
+        # Built on-demand with self._request_strategy.
+        self._request_strategy_obj = None
+
+    @property
+    def auth_uri(self):
+        auth_uri = self._adapter.get_endpoint(interface=auth.AUTH_INTERFACE)
+
+        # NOTE(jamielennox): This weird stripping of the prefix hack is
+        # only relevant to the legacy case. We urljoin '/' to get just the
+        # base URI as this is the original behaviour.
+        if isinstance(self._adapter.auth, _auth.AuthTokenPlugin):
+            auth_uri = urllib.parse.urljoin(auth_uri, '/').rstrip('/')
+
+        return auth_uri
+
+    @property
+    def auth_version(self):
+        return self._request_strategy.AUTH_VERSION
+
+    @property
+    def _request_strategy(self):
+        if not self._request_strategy_obj:
+            strategy_class = self._get_strategy_class()
+            self._adapter.version = strategy_class.AUTH_VERSION
+
+            self._request_strategy_obj = strategy_class(
+                self._json_request,
+                self._adapter,
+                include_service_catalog=self._include_service_catalog)
+
+        return self._request_strategy_obj
+
+    def _get_strategy_class(self):
+        if self._requested_auth_version:
+            # A specific version was requested.
+            if discover.version_match(_V3RequestStrategy.AUTH_VERSION,
+                                      self._requested_auth_version):
+                return _V3RequestStrategy
+
+            # The version isn't v3 so we don't know what to do. Just assume V2.
+            return _V2RequestStrategy
+
+        # Specific version was not requested then we fall through to
+        # discovering available versions from the server
+        for klass in _REQUEST_STRATEGIES:
+            if self._adapter.get_endpoint(version=klass.AUTH_VERSION):
+                msg = _LI('Auth Token confirmed use of %s apis')
+                self._LOG.info(msg, self._requested_auth_version)
+                return klass
+
+        versions = ['v%d.%d' % s.AUTH_VERSION for s in _REQUEST_STRATEGIES]
+        self._LOG.error(_LE('No attempted versions [%s] supported by server'),
+                        ', '.join(versions))
+
+        msg = _('No compatible apis supported by server')
+        raise exc.ServiceError(msg)
+
+    def verify_token(self, user_token, retry=True):
+        """Authenticate user token with identity server.
+
+        :param user_token: user's token id
+        :param retry: flag that forces the middleware to retry
+                      user authentication when an indeterminate
+                      response is received. Optional.
+        :returns: token object received from identity server on success
+        :raises exc.InvalidToken: if token is rejected
+        :raises exc.ServiceError: if unable to authenticate token
+
+        """
+        user_token = _utils.safe_quote(user_token)
+
+        try:
+            response, data = self._request_strategy.verify_token(user_token)
+        except exceptions.NotFound as e:
+            self._LOG.warn(_LW('Authorization failed for token'))
+            self._LOG.warn(_LW('Identity response: %s'), e.response.text)
+        except exceptions.Unauthorized as e:
+            self._LOG.info(_LI('Identity server rejected authorization'))
+            self._LOG.warn(_LW('Identity response: %s'), e.response.text)
+            if retry:
+                self._LOG.info(_LI('Retrying validation'))
+                return self.verify_token(user_token, False)
+        except exceptions.HttpError as e:
+            self._LOG.error(
+                _LE('Bad response code while validating token: %s'),
+                e.http_status)
+            self._LOG.warn(_LW('Identity response: %s'), e.response.text)
+        else:
+            if response.status_code == 200:
+                return data
+
+            raise exc.InvalidToken()
+
+    def fetch_revocation_list(self):
+        try:
+            response, data = self._json_request(
+                'GET', '/tokens/revoked',
+                authenticated=True,
+                endpoint_filter={'version': (2, 0)})
+        except exceptions.HTTPError as e:
+            msg = _('Failed to fetch token revocation list: %d')
+            raise exc.RevocationListError(msg % e.http_status)
+        if response.status_code != 200:
+            msg = _('Unable to fetch token revocation list.')
+            raise exc.RevocationListError(msg)
+        if 'signed' not in data:
+            msg = _('Revocation list improperly formatted.')
+            raise exc.RevocationListError(msg)
+        return data['signed']
+
+    def fetch_signing_cert(self):
+        return self._fetch_cert_file('signing')
+
+    def fetch_ca_cert(self):
+        return self._fetch_cert_file('ca')
+
+    def _json_request(self, method, path, **kwargs):
+        """HTTP request helper used to make json requests.
+
+        :param method: http method
+        :param path: relative request url
+        :param **kwargs: additional parameters used by session or endpoint
+        :returns: http response object, response body parsed as json
+        :raises ServerError: when unable to communicate with identity server.
+
+        """
+        headers = kwargs.setdefault('headers', {})
+        headers['Accept'] = 'application/json'
+
+        response = self._adapter.request(path, method, **kwargs)
+
+        try:
+            data = jsonutils.loads(response.text)
+        except ValueError:
+            self._LOG.debug('Identity server did not return json-encoded body')
+            data = {}
+
+        return response, data
+
+    def _fetch_cert_file(self, cert_type):
+        try:
+            response = self._request_strategy.fetch_cert_file(cert_type)
+        except exceptions.HTTPError as e:
+            raise exceptions.CertificateConfigError(e.details)
+        if response.status_code != 200:
+            raise exceptions.CertificateConfigError(response.text)
+        return response.text
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_crypt.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_crypt.py
new file mode 100644 (file)
index 0000000..2e45571
--- /dev/null
@@ -0,0 +1,210 @@
+# Copyright 2010-2013 OpenStack Foundation
+#
+# 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 should be serialized before entering these functions. Encryption
+has a dependency on the pycrypto. If pycrypto is not available,
+CryptoUnavailableError will be raised.
+
+This module will not be called unless signing or encryption is enabled
+in the config. It will always validate signatures, and will decrypt
+data if encryption is enabled. It is not valid to mix protection
+modes.
+
+"""
+
+import base64
+import functools
+import hashlib
+import hmac
+import math
+import os
+import six
+import sys
+
+from keystonemiddleware.i18n import _
+
+# make sure pycrypto is available
+try:
+    from Crypto.Cipher import AES
+except ImportError:
+    AES = None
+
+HASH_FUNCTION = hashlib.sha384
+DIGEST_LENGTH = HASH_FUNCTION().digest_size
+DIGEST_SPLIT = DIGEST_LENGTH // 3
+DIGEST_LENGTH_B64 = 4 * int(math.ceil(DIGEST_LENGTH / 3.0))
+
+
+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
+
+
+if sys.version_info >= (3, 3):
+    constant_time_compare = hmac.compare_digest
+else:
+    def constant_time_compare(first, second):
+        """Returns True if both string inputs are equal, otherwise False.
+
+        This function should take a constant amount of time regardless of
+        how many characters in the strings match.
+
+        """
+        if len(first) != len(second):
+            return False
+        result = 0
+        if six.PY3 and isinstance(first, bytes) and isinstance(second, bytes):
+            for x, y in zip(first, second):
+                result |= x ^ y
+        else:
+            for x, y in zip(first, second):
+                result |= ord(x) ^ ord(y)
+        return result == 0
+
+
+def derive_keys(token, secret, strategy):
+    """Derives keys for MAC and ENCRYPTION from the user-provided
+    secret. The resulting keys should be passed to the protect and
+    unprotect functions.
+
+    As suggested by NIST Special Publication 800-108, this uses the
+    first 128 bits from the sha384 KDF for the obscured cache key
+    value, the second 128 bits for the message authentication key and
+    the remaining 128 bits for the encryption key.
+
+    This approach is faster than computing a separate hmac as the KDF
+    for each desired key.
+    """
+    digest = hmac.new(secret, token + strategy, HASH_FUNCTION).digest()
+    return {'CACHE_KEY': digest[:DIGEST_SPLIT],
+            'MAC': digest[DIGEST_SPLIT: 2 * DIGEST_SPLIT],
+            'ENCRYPTION': digest[2 * DIGEST_SPLIT:],
+            'strategy': strategy}
+
+
+def sign_data(key, data):
+    """Sign the data using the defined function and the derived key."""
+    mac = hmac.new(key, data, HASH_FUNCTION).digest()
+    return base64.b64encode(mac)
+
+
+@assert_crypto_availability
+def encrypt_data(key, data):
+    """Encrypt the data with the given secret key.
+
+    Padding is n bytes of the value n, where 1 <= n <= blocksize.
+    """
+    iv = os.urandom(16)
+    cipher = AES.new(key, AES.MODE_CBC, iv)
+    padding = 16 - len(data) % 16
+    return iv + cipher.encrypt(data + six.int2byte(padding) * padding)
+
+
+@assert_crypto_availability
+def decrypt_data(key, data):
+    """Decrypt the data with the given secret key."""
+    iv = data[:16]
+    cipher = AES.new(key, AES.MODE_CBC, iv)
+    try:
+        result = cipher.decrypt(data[16:])
+    except Exception:
+        raise DecryptError(_('Encrypted data appears to be corrupted.'))
+
+    # Strip the last n padding bytes where n is the last value in
+    # the plaintext
+    return result[:-1 * six.byte2int([result[-1]])]
+
+
+def protect_data(keys, data):
+    """Given keys and serialized data, returns an appropriately
+    protected string suitable for storage in the cache.
+
+    """
+    if keys['strategy'] == b'ENCRYPT':
+        data = encrypt_data(keys['ENCRYPTION'], data)
+
+    encoded_data = base64.b64encode(data)
+
+    signature = sign_data(keys['MAC'], encoded_data)
+    return signature + encoded_data
+
+
+def unprotect_data(keys, signed_data):
+    """Given keys and cached string data, verifies the signature,
+    decrypts if necessary, and returns the original serialized data.
+
+    """
+    # cache backends return None when no data is found. We don't mind
+    # that this particular special value is unsigned.
+    if signed_data is None:
+        return None
+
+    # First we calculate the signature
+    provided_mac = signed_data[:DIGEST_LENGTH_B64]
+    calculated_mac = sign_data(
+        keys['MAC'],
+        signed_data[DIGEST_LENGTH_B64:])
+
+    # Then verify that it matches the provided value
+    if not constant_time_compare(provided_mac, calculated_mac):
+        raise InvalidMacError(_('Invalid MAC; data appears to be corrupted.'))
+
+    data = base64.b64decode(signed_data[DIGEST_LENGTH_B64:])
+
+    # then if necessary decrypt the data
+    if keys['strategy'] == b'ENCRYPT':
+        data = decrypt_data(keys['ENCRYPTION'], data)
+
+    return data
+
+
+def get_cache_key(keys):
+    """Given keys generated by derive_keys(), returns a base64
+    encoded value suitable for use as a cache key in memcached.
+
+    """
+    return base64.b64encode(keys['CACHE_KEY'])
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_pool.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_pool.py
new file mode 100644 (file)
index 0000000..7765286
--- /dev/null
@@ -0,0 +1,184 @@
+# Copyright 2014 Mirantis Inc
+# 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.
+
+"""Thread-safe connection pool for python-memcached."""
+
+# NOTE(yorik-sar): this file is copied between keystone and keystonemiddleware
+# and should be kept in sync until we can use external library for this.
+
+import collections
+import contextlib
+import itertools
+import logging
+import time
+
+from six.moves import queue
+
+from keystonemiddleware.i18n import _LC
+
+
+_PoolItem = collections.namedtuple('_PoolItem', ['ttl', 'connection'])
+
+
+class ConnectionGetTimeoutException(Exception):
+    pass
+
+
+class ConnectionPool(queue.Queue):
+    """Base connection pool class
+
+    This class implements the basic connection pool logic as an abstract base
+    class.
+    """
+    def __init__(self, maxsize, unused_timeout, conn_get_timeout=None):
+        """Initialize the connection pool.
+
+        :param maxsize: maximum number of client connections for the pool
+        :type maxsize: int
+        :param unused_timeout: idle time to live for unused clients (in
+                               seconds). If a client connection object has been
+                               in the pool and idle for longer than the
+                               unused_timeout, it will be reaped. This is to
+                               ensure resources are released as utilization
+                               goes down.
+        :type unused_timeout: int
+        :param conn_get_timeout: maximum time in seconds to wait for a
+                                 connection. If set to `None` timeout is
+                                 indefinite.
+        :type conn_get_timeout: int
+        """
+        queue.Queue.__init__(self, maxsize)
+        self._unused_timeout = unused_timeout
+        self._connection_get_timeout = conn_get_timeout
+        self._acquired = 0
+        self._LOG = logging.getLogger(__name__)
+
+    def _create_connection(self):
+        raise NotImplementedError
+
+    def _destroy_connection(self, conn):
+        raise NotImplementedError
+
+    @contextlib.contextmanager
+    def acquire(self):
+        try:
+            conn = self.get(timeout=self._connection_get_timeout)
+        except queue.Empty:
+            self._LOG.critical(_LC('Unable to get a connection from pool id '
+                                   '%(id)s after %(seconds)s seconds.'),
+                               {'id': id(self),
+                                'seconds': self._connection_get_timeout})
+            raise ConnectionGetTimeoutException()
+        try:
+            yield conn
+        finally:
+            self.put(conn)
+
+    def _qsize(self):
+        return self.maxsize - self._acquired
+
+    if not hasattr(queue.Queue, '_qsize'):
+        qsize = _qsize
+
+    def _get(self):
+        if self.queue:
+            conn = self.queue.pop().connection
+        else:
+            conn = self._create_connection()
+        self._acquired += 1
+        return conn
+
+    def _put(self, conn):
+        self.queue.append(_PoolItem(
+            ttl=time.time() + self._unused_timeout,
+            connection=conn,
+        ))
+        self._acquired -= 1
+        # Drop all expired connections from the right end of the queue
+        now = time.time()
+        while self.queue and self.queue[0].ttl < now:
+            conn = self.queue.popleft().connection
+            self._destroy_connection(conn)
+
+
+class MemcacheClientPool(ConnectionPool):
+    def __init__(self, urls, arguments, **kwargs):
+        ConnectionPool.__init__(self, **kwargs)
+        self._urls = urls
+        self._arguments = arguments
+        # NOTE(morganfainberg): The host objects expect an int for the
+        # deaduntil value. Initialize this at 0 for each host with 0 indicating
+        # the host is not dead.
+        self._hosts_deaduntil = [0] * len(urls)
+
+        # NOTE(morganfainberg): Lazy import to allow middleware to work with
+        # python 3k even if memcache will not due to python 3k
+        # incompatibilities within the python-memcache library.
+        global memcache
+        import memcache
+
+        # This 'class' is taken from http://stackoverflow.com/a/22520633/238308
+        # Don't inherit client from threading.local so that we can reuse
+        # clients in different threads
+        MemcacheClient = type('_MemcacheClient', (object,),
+                              dict(memcache.Client.__dict__))
+
+        self._memcache_client_class = MemcacheClient
+
+    def _create_connection(self):
+        return self._memcache_client_class(self._urls, **self._arguments)
+
+    def _destroy_connection(self, conn):
+        conn.disconnect_all()
+
+    def _get(self):
+        conn = ConnectionPool._get(self)
+        try:
+            # Propagate host state known to us to this client's list
+            now = time.time()
+            for deaduntil, host in zip(self._hosts_deaduntil, conn.servers):
+                if deaduntil > now and host.deaduntil <= now:
+                    host.mark_dead('propagating death mark from the pool')
+                host.deaduntil = deaduntil
+        except Exception:
+            # We need to be sure that connection doesn't leak from the pool.
+            # This code runs before we enter context manager's try-finally
+            # block, so we need to explicitly release it here
+            ConnectionPool._put(self, conn)
+            raise
+        return conn
+
+    def _put(self, conn):
+        try:
+            # If this client found that one of the hosts is dead, mark it as
+            # such in our internal list
+            now = time.time()
+            for i, deaduntil, host in zip(itertools.count(),
+                                          self._hosts_deaduntil,
+                                          conn.servers):
+                # Do nothing if we already know this host is dead
+                if deaduntil <= now:
+                    if host.deaduntil > now:
+                        self._hosts_deaduntil[i] = host.deaduntil
+                    else:
+                        self._hosts_deaduntil[i] = 0
+            # If all hosts are dead we should forget that they're dead. This
+            # way we won't get completely shut off until dead_retry seconds
+            # pass, but will be checking servers as frequent as we can (over
+            # way smaller socket_timeout)
+            if all(deaduntil > now for deaduntil in self._hosts_deaduntil):
+                self._hosts_deaduntil[:] = [0] * len(self._hosts_deaduntil)
+        finally:
+            ConnectionPool._put(self, conn)
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_revocations.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_revocations.py
new file mode 100644 (file)
index 0000000..8cc449a
--- /dev/null
@@ -0,0 +1,106 @@
+# 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.
+
+import datetime
+import logging
+import os
+
+from oslo_serialization import jsonutils
+from oslo_utils import timeutils
+
+from keystonemiddleware.auth_token import _exceptions as exc
+from keystonemiddleware.i18n import _
+
+_LOG = logging.getLogger(__name__)
+
+
+class Revocations(object):
+    _FILE_NAME = 'revoked.pem'
+
+    def __init__(self, timeout, signing_directory, identity_server,
+                 cms_verify, log=_LOG):
+        self._cache_timeout = timeout
+        self._signing_directory = signing_directory
+        self._identity_server = identity_server
+        self._cms_verify = cms_verify
+        self._log = log
+
+        self._fetched_time_prop = None
+        self._list_prop = None
+
+    @property
+    def _fetched_time(self):
+        if not self._fetched_time_prop:
+            # If the fetched list has been written to disk, use its
+            # modification time.
+            file_path = self._signing_directory.calc_path(self._FILE_NAME)
+            if os.path.exists(file_path):
+                mtime = os.path.getmtime(file_path)
+                fetched_time = datetime.datetime.utcfromtimestamp(mtime)
+            # Otherwise the list will need to be fetched.
+            else:
+                fetched_time = datetime.datetime.min
+            self._fetched_time_prop = fetched_time
+        return self._fetched_time_prop
+
+    @_fetched_time.setter
+    def _fetched_time(self, value):
+        self._fetched_time_prop = value
+
+    def _fetch(self):
+        revocation_list_data = self._identity_server.fetch_revocation_list()
+        return self._cms_verify(revocation_list_data)
+
+    @property
+    def _list(self):
+        timeout = self._fetched_time + self._cache_timeout
+        list_is_current = timeutils.utcnow() < timeout
+
+        if list_is_current:
+            # Load the list from disk if required
+            if not self._list_prop:
+                self._list_prop = jsonutils.loads(
+                    self._signing_directory.read_file(self._FILE_NAME))
+        else:
+            self._list = self._fetch()
+        return self._list_prop
+
+    @_list.setter
+    def _list(self, value):
+        """Save a revocation list to memory and to disk.
+
+        :param value: A json-encoded revocation list
+
+        """
+        self._list_prop = jsonutils.loads(value)
+        self._fetched_time = timeutils.utcnow()
+        self._signing_directory.write_file(self._FILE_NAME, value)
+
+    def _is_revoked(self, token_id):
+        """Indicate whether the token_id appears in the revocation list."""
+        revoked_tokens = self._list.get('revoked', None)
+        if not revoked_tokens:
+            return False
+
+        revoked_ids = (x['id'] for x in revoked_tokens)
+        return token_id in revoked_ids
+
+    def _any_revoked(self, token_ids):
+        for token_id in token_ids:
+            if self._is_revoked(token_id):
+                return True
+        return False
+
+    def check(self, token_ids):
+        if self._any_revoked(token_ids):
+            self._log.debug('Token is marked as having been revoked')
+            raise exc.InvalidToken(_('Token has been revoked'))
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_signing_dir.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_signing_dir.py
new file mode 100644 (file)
index 0000000..f8b1a41
--- /dev/null
@@ -0,0 +1,83 @@
+# 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.
+
+import logging
+import os
+import stat
+import tempfile
+
+import six
+
+from keystonemiddleware.auth_token import _exceptions as exc
+from keystonemiddleware.i18n import _, _LI, _LW
+
+_LOG = logging.getLogger(__name__)
+
+
+class SigningDirectory(object):
+
+    def __init__(self, directory_name=None, log=None):
+        self._log = log or _LOG
+
+        if directory_name is None:
+            directory_name = tempfile.mkdtemp(prefix='keystone-signing-')
+        self._log.info(
+            _LI('Using %s as cache directory for signing certificate'),
+            directory_name)
+        self._directory_name = directory_name
+
+        self._verify_signing_dir()
+
+    def write_file(self, file_name, new_contents):
+
+        # In Python2, encoding is slow so the following check avoids it if it
+        # is not absolutely necessary.
+        if isinstance(new_contents, six.text_type):
+            new_contents = new_contents.encode('utf-8')
+
+        def _atomic_write():
+            with tempfile.NamedTemporaryFile(dir=self._directory_name,
+                                             delete=False) as f:
+                f.write(new_contents)
+            os.rename(f.name, self.calc_path(file_name))
+
+        try:
+            _atomic_write()
+        except (OSError, IOError):
+            self._verify_signing_dir()
+            _atomic_write()
+
+    def read_file(self, file_name):
+        path = self.calc_path(file_name)
+        open_kwargs = {'encoding': 'utf-8'} if six.PY3 else {}
+        with open(path, 'r', **open_kwargs) as f:
+            return f.read()
+
+    def calc_path(self, file_name):
+        return os.path.join(self._directory_name, file_name)
+
+    def _verify_signing_dir(self):
+        if os.path.isdir(self._directory_name):
+            if not os.access(self._directory_name, os.W_OK):
+                raise exc.ConfigurationError(
+                    _('unable to access signing_dir %s') %
+                    self._directory_name)
+            uid = os.getuid()
+            if os.stat(self._directory_name).st_uid != uid:
+                self._log.warning(_LW('signing_dir is not owned by %s'), uid)
+            current_mode = stat.S_IMODE(os.stat(self._directory_name).st_mode)
+            if current_mode != stat.S_IRWXU:
+                self._log.warning(
+                    _LW('signing_dir mode is %(mode)s instead of %(need)s'),
+                    {'mode': oct(current_mode), 'need': oct(stat.S_IRWXU)})
+        else:
+            os.makedirs(self._directory_name, stat.S_IRWXU)
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_user_plugin.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_user_plugin.py
new file mode 100644 (file)
index 0000000..12a8767
--- /dev/null
@@ -0,0 +1,169 @@
+# 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.
+
+from keystoneclient.auth.identity import base as base_identity
+
+
+class _TokenData(object):
+    """An abstraction to show auth_token consumers some of the token contents.
+
+    This is a simplified and cleaned up keystoneclient.access.AccessInfo object
+    with which services relying on auth_token middleware can find details of
+    the current token.
+    """
+
+    def __init__(self, auth_ref):
+        self._stored_auth_ref = auth_ref
+
+    @property
+    def _is_v2(self):
+        return self._stored_auth_ref.version == 'v2.0'
+
+    @property
+    def auth_token(self):
+        """The token data used to authenticate requests.
+
+        :returns: token data.
+        :rtype: str
+        """
+        return self._stored_auth_ref.auth_token
+
+    @property
+    def user_id(self):
+        """The user id associated with the authentication request.
+
+        :rtype: str
+        """
+        return self._stored_auth_ref.user_id
+
+    @property
+    def user_domain_id(self):
+        """Returns the domain id of the user associated with the authentication
+        request.
+
+        :returns: str
+        """
+        # NOTE(jamielennox): v2 AccessInfo returns 'default' for domain_id
+        # because it can't know that value. We want to return None instead.
+        if self._is_v2:
+            return None
+
+        return self._stored_auth_ref.user_domain_id
+
+    @property
+    def project_id(self):
+        """The project ID associated with the authentication.
+
+        :rtype: str
+        """
+        return self._stored_auth_ref.project_id
+
+    @property
+    def project_domain_id(self):
+        """The domain id of the project associated with the authentication
+        request.
+
+        :rtype: str
+        """
+        # NOTE(jamielennox): v2 AccessInfo returns 'default' for domain_id
+        # because it can't know that value. We want to return None instead.
+        if self._is_v2:
+            return None
+
+        return self._stored_auth_ref.project_domain_id
+
+    @property
+    def trust_id(self):
+        """Returns the trust id associated with the authentication request..
+
+        :rtype: str
+        """
+        return self._stored_auth_ref.trust_id
+
+    @property
+    def role_ids(self):
+        """Role ids of the user associated with the authentication request.
+
+        :rtype: set(str)
+        """
+        return frozenset(self._stored_auth_ref.role_ids or [])
+
+    @property
+    def role_names(self):
+        """Role names of the user associated with the authentication request.
+
+        :rtype: set(str)
+        """
+        return frozenset(self._stored_auth_ref.role_names or [])
+
+
+class UserAuthPlugin(base_identity.BaseIdentityPlugin):
+    """The incoming authentication credentials.
+
+    A plugin that represents the incoming user credentials. This can be
+    consumed by applications.
+
+    This object is not expected to be constructed directly by users. It is
+    created and passed by auth_token middleware and then can be used as the
+    authentication plugin when communicating via a session.
+    """
+
+    def __init__(self, user_auth_ref, serv_auth_ref):
+        super(UserAuthPlugin, self).__init__(reauthenticate=False)
+        self._user_auth_ref = user_auth_ref
+        self._serv_auth_ref = serv_auth_ref
+        self._user_data = None
+        self._serv_data = None
+
+    @property
+    def has_user_token(self):
+        """Did this authentication request contained a user auth token."""
+        return self._user_auth_ref is not None
+
+    @property
+    def user(self):
+        """Authentication information about the user token.
+
+        Will return None if a user token was not passed with this request.
+        """
+        if not self.has_user_token:
+            return None
+
+        if not self._user_data:
+            self._user_data = _TokenData(self._user_auth_ref)
+
+        return self._user_data
+
+    @property
+    def has_service_token(self):
+        """Did this authentication request contained a service token."""
+        return self._serv_auth_ref is not None
+
+    @property
+    def service(self):
+        """Authentication information about the service token.
+
+        Will return None if a user token was not passed with this request.
+        """
+        if not self.has_service_token:
+            return None
+
+        if not self._serv_data:
+            self._serv_data = _TokenData(self._serv_auth_ref)
+
+        return self._serv_data
+
+    def get_auth_ref(self, session, **kwargs):
+        # NOTE(jamielennox): We will always use the auth_ref that was
+        # calculated by the middleware. reauthenticate=False in __init__ should
+        # ensure that this function is only called on the first access.
+        return self._user_auth_ref
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_utils.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_utils.py
new file mode 100644 (file)
index 0000000..daed02d
--- /dev/null
@@ -0,0 +1,32 @@
+# 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.
+
+from six.moves import urllib
+
+
+def safe_quote(s):
+    """URL-encode strings that are not already URL-encoded."""
+    return urllib.parse.quote(s) if s == urllib.parse.unquote(s) else s
+
+
+class MiniResp(object):
+
+    def __init__(self, error_message, env, headers=[]):
+        # The HEAD method is unique: it must never return a body, even if
+        # it reports an error (RFC-2616 clause 9.4). We relieve callers
+        # from varying the error responses depending on the method.
+        if env['REQUEST_METHOD'] == 'HEAD':
+            self.body = ['']
+        else:
+            self.body = [error_message.encode()]
+        self.headers = list(headers)
+        self.headers.append(('Content-type', 'text/plain'))
diff --git a/keystonemiddleware-moon/keystonemiddleware/authz.py b/keystonemiddleware-moon/keystonemiddleware/authz.py
new file mode 100644 (file)
index 0000000..f969b2c
--- /dev/null
@@ -0,0 +1,326 @@
+# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors
+# This software is distributed under the terms and conditions of the 'Apache-2.0'
+# license which can be found in the file 'LICENSE' in this package distribution
+# or at 'http://www.apache.org/licenses/LICENSE-2.0'.
+
+import webob
+import logging
+import json
+import six
+import requests
+import re
+import httplib
+
+from keystone import exception
+from cStringIO import StringIO
+from oslo.config import cfg
+# from keystoneclient import auth
+from keystonemiddleware.i18n import _, _LC, _LE, _LI, _LW
+
+
+_OPTS = [
+    cfg.StrOpt('auth_uri',
+               default="http://127.0.0.1:35357/v3",
+               help='Complete public Identity API endpoint.'),
+    cfg.StrOpt('auth_version',
+               default=None,
+               help='API version of the admin Identity API endpoint.'),
+    cfg.StrOpt('authz_login',
+               default="admin",
+               help='Name of the administrator who will connect to the Keystone Moon backends.'),
+    cfg.StrOpt('authz_password',
+               default="nomoresecrete",
+               help='Password of the administrator who will connect to the Keystone Moon backends.'),
+    cfg.StrOpt('logfile',
+               default="/tmp/authz.log",
+               help='File where logs goes.'),
+    ]
+
+_AUTHZ_GROUP = 'keystone_authz'
+CONF = cfg.CONF
+CONF.register_opts(_OPTS, group=_AUTHZ_GROUP)
+# auth.register_conf_options(CONF, _AUTHZ_GROUP)
+
+# from http://developer.openstack.org/api-ref-objectstorage-v1.html
+SWIFT_API = (
+    ("^/v1/(?P<account>[\w-]+)$", "GET", "get_account_details"),
+    ("^/v1/(?P<account>[\w-]+)$", "POST", "modify_account"),
+    ("^/v1/(?P<account>[\w-]+)$", "HEAD", "get_account"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)$", "GET", "get_container"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)$", "PUT", "create_container"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)$", "POST", "update_container_metadata"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)$", "DELETE", "delete_container"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)$", "HEAD", "get_container_metadata"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)/(?P<object>[\w-]+)$", "GET", "get_object"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)/(?P<object>[\w-]+)$", "PUT", "create_object"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)/(?P<object>[\w-]+)$", "COPY", "copy_object"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)/(?P<object>[\w-]+)$", "POST", "update_object_metadata"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)/(?P<object>[\w-]+)$", "DELETE", "delete_object"),
+    ("^/v1/(?P<account>[\w-]+)/(?P<container>[\w-]+)/(?P<object>[\w-]+)$", "HEAD", "get_object_metadata"),
+)
+
+
+class ServiceError(Exception):
+    pass
+
+
+class AuthZProtocol(object):
+    """Middleware that handles authenticating client calls."""
+
+    post = {
+        "auth": {
+            "identity": {
+                "methods": [
+                    "password"
+                ],
+                "password": {
+                    "user": {
+                        "domain": {
+                            "id": "Default"
+                        },
+                        "name": "admin",
+                        "password": "nomoresecrete"
+                    }
+                }
+            },
+            "scope": {
+                "project": {
+                    "domain": {
+                        "id": "Default"
+                    },
+                    "name": "demo"
+                }
+            }
+        }
+    }
+
+    def __init__(self, app, conf):
+        self._LOG = logging.getLogger(conf.get('log_name', __name__))
+        # FIXME: events are duplicated in log file
+        authz_fh = logging.FileHandler(CONF.keystone_authz["logfile"])
+        self._LOG.setLevel(logging.DEBUG)
+        self._LOG.addHandler(authz_fh)
+        self._LOG.info(_LI('Starting Keystone authz middleware'))
+        self._conf = conf
+        self._app = app
+
+        # MOON
+        self.auth_host = conf.get('auth_host', "127.0.0.1")
+        self.auth_port = int(conf.get('auth_port', 35357))
+        auth_protocol = conf.get('auth_protocol', 'http')
+        self._request_uri = '%s://%s:%s' % (auth_protocol, self.auth_host,
+                                            self.auth_port)
+
+        # SSL
+        insecure = conf.get('insecure', False)
+        cert_file = conf.get('certfile')
+        key_file = conf.get('keyfile')
+
+        if insecure:
+            self._verify = False
+        elif cert_file and key_file:
+            self._verify = (cert_file, key_file)
+        elif cert_file:
+            self._verify = cert_file
+        else:
+            self._verify = None
+
+    def __set_token(self):
+        data = self.get_url("/v3/auth/tokens", post_data=self.post)
+        if "token" not in data:
+            raise Exception("Authentication problem ({})".format(data))
+        self.token = data["token"]
+
+    def __unset_token(self):
+        data = self.get_url("/v3/auth/tokens", method="DELETE", authtoken=True)
+        if "content" in data and len(data["content"]) > 0:
+            self._LOG.error("Error while unsetting token {}".format(data["content"]))
+        self.token = None
+
+    def get_url(self, url, post_data=None, delete_data=None, method="GET", authtoken=None):
+        if post_data:
+            method = "POST"
+        if delete_data:
+            method = "DELETE"
+        self._LOG.debug("\033[32m{} {}\033[m".format(method, url))
+        conn = httplib.HTTPConnection(self.auth_host, self.auth_port)
+        headers = {
+            "Content-type": "application/x-www-form-urlencoded",
+            "Accept": "text/plain,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+        }
+        if authtoken:
+            if self.x_subject_token:
+                if method == "DELETE":
+                    headers["X-Subject-Token"] = self.x_subject_token
+                    headers["X-Auth-Token"] = self.x_subject_token
+                else:
+                    headers["X-Auth-Token"] = self.x_subject_token
+        if post_data:
+            method = "POST"
+            headers["Content-type"] = "application/json"
+            post_data = json.dumps(post_data)
+            conn.request(method, url, post_data, headers=headers)
+        elif delete_data:
+            method = "DELETE"
+            conn.request(method, url, json.dumps(delete_data), headers=headers)
+        else:
+            conn.request(method, url, headers=headers)
+        resp = conn.getresponse()
+        headers = resp.getheaders()
+        try:
+            self.x_subject_token = dict(headers)["x-subject-token"]
+        except KeyError:
+            pass
+        content = resp.read()
+        conn.close()
+        try:
+            return json.loads(content)
+        except ValueError:
+            return {"content": content}
+
+    def _deny_request(self, code):
+        error_table = {
+            'AccessDenied': (401, 'Access denied'),
+            'InvalidURI': (400, 'Could not parse the specified URI'),
+            'NotFound': (404, 'URI not found'),
+            'Error': (500, 'Server error'),
+        }
+        resp = webob.Response(content_type='text/xml')
+        resp.status = error_table[code][0]
+        error_msg = ('<?xml version="1.0" encoding="UTF-8"?>\r\n'
+                     '<Error>\r\n  <Code>%s</Code>\r\n  '
+                     '<Message>%s</Message>\r\n</Error>\r\n' %
+                     (code, error_table[code][1]))
+        if six.PY3:
+            error_msg = error_msg.encode()
+        resp.body = error_msg
+        return resp
+
+    def _get_authz_from_moon(self, auth_token, tenant_id, subject_id, object_id, action_id):
+        headers = {'X-Auth-Token': auth_token}
+        self._LOG.debug('X-Auth-Token={}'.format(auth_token))
+        try:
+            _url ='{}/v3/OS-MOON/authz/{}/{}/{}/{}'.format(
+                                        self._request_uri,
+                                        tenant_id,
+                                        subject_id,
+                                        object_id,
+                                        action_id)
+            self._LOG.info(_url)
+            response = requests.get(_url,
+                                    headers=headers,
+                                    verify=self._verify)
+        except requests.exceptions.RequestException as e:
+            self._LOG.error(_LI('HTTP connection exception: %s'), e)
+            resp = self._deny_request('InvalidURI')
+            raise ServiceError(resp)
+
+        if response.status_code < 200 or response.status_code >= 300:
+            self._LOG.debug('Keystone reply error: status=%s reason=%s',
+                               response.status_code, response.reason)
+            if response.status_code == 404:
+                resp = self._deny_request('NotFound')
+            elif response.status_code == 401:
+                resp = self._deny_request('AccessDenied')
+            else:
+                resp = self._deny_request('Error')
+            raise ServiceError(resp)
+
+        return response
+
+    def _find_openstack_component(self, env):
+        if "nova.context" in env.keys():
+            return "nova"
+        elif "swift.authorize" in env.keys():
+            return "swift"
+        else:
+            self._LOG.debug(env.keys())
+            return "unknown"
+
+    def _get_action(self, env, component):
+        """ Find and return the action of the request
+        Actually, find only Nova action (start, destroy, pause, unpause, ...)
+
+        :param env: the request
+        :return: the action or ""
+        """
+        action = ""
+        if component == "nova":
+            length = int(env.get('CONTENT_LENGTH', '0'))
+            # TODO (dthom): compute for Nova, Cinder, Neutron, ...
+            action = ""
+            if length > 0:
+                try:
+                    sub_action_object = env['wsgi.input'].read(length)
+                    action = json.loads(sub_action_object).keys()[0]
+                    body = StringIO(sub_action_object)
+                    env['wsgi.input'] = body
+                except ValueError:
+                    self._LOG.error("Error in decoding sub-action")
+                except Exception as e:
+                    self._LOG.error(str(e))
+            if not action or len(action) == 0 and "servers/detail" in env["PATH_INFO"]:
+                return "list"
+        if component == "swift":
+            path = env["PATH_INFO"]
+            method = env["REQUEST_METHOD"]
+            for api in SWIFT_API:
+                if re.match(api[0], path) and method == api[1]:
+                    action = api[2]
+        return action
+
+    @staticmethod
+    def _get_object(env, component):
+        if component == "nova":
+            # get the object ID which is located before "action" in the URL
+            return env.get("PATH_INFO").split("/")[-2]
+        elif component == "swift":
+            # remove the "/v1/" part of the URL
+            return env.get("PATH_INFO").split("/", 2)[-1].replace("/", "-")
+        return "unknown"
+
+    def __call__(self, env, start_response):
+        req = webob.Request(env)
+
+        # token = req.headers.get('X-Auth-Token',
+        #                         req.headers.get('X-Storage-Token'))
+        # if not token:
+        #     self._LOG.error("No token")
+        #     return self._app(env, start_response)
+
+        subject_id = env.get("HTTP_X_USER_ID")
+        tenant_id = env.get("HTTP_X_TENANT_ID")
+        component = self._find_openstack_component(env)
+        action_id = self._get_action(env, component)
+        if action_id:
+            self._LOG.debug("OpenStack component {}".format(component))
+            object_id = self._get_object(env, component)
+            self._LOG.debug("{}-{}-{}-{}".format(subject_id, object_id, action_id, tenant_id))
+            self.__set_token()
+            resp = self._get_authz_from_moon(self.x_subject_token, tenant_id, subject_id, object_id, action_id)
+            self._LOG.info("Moon answer: {}-{}".format(resp.status_code, resp.content))
+            self.__unset_token()
+            if resp.status_code == 200:
+                try:
+                    answer = json.loads(resp.content)
+                    self._LOG.debug(answer)
+                    if "authz" in answer and answer["authz"]:
+                        return self._app(env, start_response)
+                except:
+                    raise exception.Unauthorized(message="You are not authorized to do that!")
+        self._LOG.debug("No action_id found for {}".format(env.get("PATH_INFO")))
+        # If action is not found, we can't raise an exception because a lots of action is missing
+        # in function self._get_action, it is not possible to get them all.
+        return self._app(env, start_response)
+        # raise exception.Unauthorized(message="You are not authorized to do that!")
+
+
+def filter_factory(global_conf, **local_conf):
+    """Returns a WSGI filter app for use with paste.deploy."""
+    conf = global_conf.copy()
+    conf.update(local_conf)
+
+    def auth_filter(app):
+        return AuthZProtocol(app, conf)
+    return auth_filter
+
diff --git a/keystonemiddleware-moon/keystonemiddleware/ec2_token.py b/keystonemiddleware-moon/keystonemiddleware/ec2_token.py
new file mode 100644 (file)
index 0000000..df3bb6b
--- /dev/null
@@ -0,0 +1,130 @@
+# Copyright 2012 OpenStack Foundation
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+"""
+Starting point for routing EC2 requests.
+
+"""
+
+from oslo_config import cfg
+from oslo_serialization import jsonutils
+import requests
+import webob.dec
+import webob.exc
+
+keystone_ec2_opts = [
+    cfg.StrOpt('url',
+               default='http://localhost:5000/v2.0/ec2tokens',
+               help='URL to get token from ec2 request.'),
+    cfg.StrOpt('keyfile',
+               help='Required if EC2 server requires client certificate.'),
+    cfg.StrOpt('certfile',
+               help='Client certificate key filename. Required if EC2 server '
+                    'requires client certificate.'),
+    cfg.StrOpt('cafile',
+               help='A PEM encoded certificate authority to use when '
+                    'verifying HTTPS connections. Defaults to the system '
+                    'CAs.'),
+    cfg.BoolOpt('insecure', default=False,
+                help='Disable SSL certificate verification.'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(keystone_ec2_opts, group='keystone_ec2_token')
+
+
+class EC2Token(object):
+    """Authenticate an EC2 request with keystone and convert to token."""
+
+    def __init__(self, application):
+        super(EC2Token, self).__init__()
+        self._application = application
+
+    @webob.dec.wsgify()
+    def __call__(self, req):
+        # Read request signature and access id.
+        try:
+            signature = req.params['Signature']
+            access = req.params['AWSAccessKeyId']
+        except KeyError:
+            raise webob.exc.HTTPBadRequest()
+
+        # Make a copy of args for authentication and signature verification.
+        auth_params = dict(req.params)
+        # Not part of authentication args
+        auth_params.pop('Signature')
+
+        # Authenticate the request.
+        creds = {
+            'ec2Credentials': {
+                'access': access,
+                'signature': signature,
+                'host': req.host,
+                'verb': req.method,
+                'path': req.path,
+                'params': auth_params,
+            }
+        }
+        creds_json = jsonutils.dumps(creds)
+        headers = {'Content-Type': 'application/json'}
+
+        verify = True
+        if CONF.keystone_ec2_token.insecure:
+            verify = False
+        elif CONF.keystone_ec2_token.cafile:
+            verify = CONF.keystone_ec2_token.cafile
+
+        cert = None
+        if (CONF.keystone_ec2_token.certfile and
+                CONF.keystone_ec2_token.keyfile):
+            cert = (CONF.keystone_ec2_certfile,
+                    CONF.keystone_ec2_token.keyfile)
+        elif CONF.keystone_ec2_token.certfile:
+            cert = CONF.keystone_ec2_token.certfile
+
+        response = requests.post(CONF.keystone_ec2_token.url, data=creds_json,
+                                 headers=headers, verify=verify, cert=cert)
+
+        # NOTE(vish): We could save a call to keystone by
+        #             having keystone return token, tenant,
+        #             user, and roles from this call.
+
+        result = response.json()
+        try:
+            token_id = result['access']['token']['id']
+        except (AttributeError, KeyError):
+            raise webob.exc.HTTPBadRequest()
+
+        # Authenticated!
+        req.headers['X-Auth-Token'] = token_id
+        return self._application
+
+
+def filter_factory(global_conf, **local_conf):
+    """Returns a WSGI filter app for use with paste.deploy."""
+    conf = global_conf.copy()
+    conf.update(local_conf)
+
+    def auth_filter(app):
+        return EC2Token(app, conf)
+    return auth_filter
+
+
+def app_factory(global_conf, **local_conf):
+    conf = global_conf.copy()
+    conf.update(local_conf)
+    return EC2Token(None, conf)
diff --git a/keystonemiddleware-moon/keystonemiddleware/i18n.py b/keystonemiddleware-moon/keystonemiddleware/i18n.py
new file mode 100644 (file)
index 0000000..0998460
--- /dev/null
@@ -0,0 +1,37 @@
+# Copyright 2014 IBM Corp.
+#
+# 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.
+
+"""oslo.i18n integration module.
+
+See http://docs.openstack.org/developer/oslo.i18n/usage.html .
+
+"""
+
+from oslo import i18n
+
+
+_translators = i18n.TranslatorFactory(domain='keystonemiddleware')
+
+# The primary translation function using the well-known name "_"
+_ = _translators.primary
+
+# Translators for log levels.
+#
+# The abbreviated names are meant to reflect the usual use of a short
+# name like '_'. The "L" is for "log" and the other letter comes from
+# the level.
+_LI = _translators.log_info
+_LW = _translators.log_warning
+_LE = _translators.log_error
+_LC = _translators.log_critical
diff --git a/keystonemiddleware-moon/keystonemiddleware/openstack/__init__.py b/keystonemiddleware-moon/keystonemiddleware/openstack/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/keystonemiddleware-moon/keystonemiddleware/openstack/common/__init__.py b/keystonemiddleware-moon/keystonemiddleware/openstack/common/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/keystonemiddleware-moon/keystonemiddleware/openstack/common/memorycache.py b/keystonemiddleware-moon/keystonemiddleware/openstack/common/memorycache.py
new file mode 100644 (file)
index 0000000..f793c93
--- /dev/null
@@ -0,0 +1,97 @@
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+"""Super simple fake memcache client."""
+
+import copy
+
+from oslo.config import cfg
+from oslo.utils import timeutils
+
+memcache_opts = [
+    cfg.ListOpt('memcached_servers',
+                help='Memcached servers or None for in process cache.'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(memcache_opts)
+
+
+def list_opts():
+    """Entry point for oslo.config-generator."""
+    return [(None, copy.deepcopy(memcache_opts))]
+
+
+def get_client(memcached_servers=None):
+    client_cls = Client
+
+    if not memcached_servers:
+        memcached_servers = CONF.memcached_servers
+    if memcached_servers:
+        import memcache
+        client_cls = memcache.Client
+
+    return client_cls(memcached_servers, debug=0)
+
+
+class Client(object):
+    """Replicates a tiny subset of memcached client interface."""
+
+    def __init__(self, *args, **kwargs):
+        """Ignores the passed in args."""
+        self.cache = {}
+
+    def get(self, key):
+        """Retrieves the value for a key or None.
+
+        This expunges expired keys during each get.
+        """
+
+        now = timeutils.utcnow_ts()
+        for k in list(self.cache):
+            (timeout, _value) = self.cache[k]
+            if timeout and now >= timeout:
+                del self.cache[k]
+
+        return self.cache.get(key, (0, None))[1]
+
+    def set(self, key, value, time=0, min_compress_len=0):
+        """Sets the value for a key."""
+        timeout = 0
+        if time != 0:
+            timeout = timeutils.utcnow_ts() + time
+        self.cache[key] = (timeout, value)
+        return True
+
+    def add(self, key, value, time=0, min_compress_len=0):
+        """Sets the value for a key if it doesn't exist."""
+        if self.get(key) is not None:
+            return False
+        return self.set(key, value, time, min_compress_len)
+
+    def incr(self, key, delta=1):
+        """Increments the value for a key."""
+        value = self.get(key)
+        if value is None:
+            return None
+        new_value = int(value) + delta
+        self.cache[key] = (self.cache[key][0], str(new_value))
+        return new_value
+
+    def delete(self, key, time=0):
+        """Deletes the value associated with a key."""
+        if key in self.cache:
+            del self.cache[key]
diff --git a/keystonemiddleware-moon/keystonemiddleware/opts.py b/keystonemiddleware-moon/keystonemiddleware/opts.py
new file mode 100644 (file)
index 0000000..62a7dab
--- /dev/null
@@ -0,0 +1,52 @@
+# Copyright (c) 2014 OpenStack Foundation.
+#
+# 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.
+
+__all__ = [
+    'list_auth_token_opts',
+]
+
+import copy
+
+import keystonemiddleware.auth_token
+from keystonemiddleware.auth_token import _auth
+from keystonemiddleware.auth_token import _base
+
+auth_token_opts = [
+    (_base.AUTHTOKEN_GROUP,
+     keystonemiddleware.auth_token._OPTS +
+     _auth.AuthTokenPlugin.get_options())
+]
+
+
+def list_auth_token_opts():
+    """Return a list of oslo_config options available in auth_token middleware.
+
+    The returned list includes all oslo_config options which may be registered
+    at runtime by the project.
+
+    Each element of the list is a tuple. The first element is the name of the
+    group under which the list of elements in the second element will be
+    registered. A group name of None corresponds to the [DEFAULT] group in
+    config files.
+
+    This function is also discoverable via the entry point
+    'keystonemiddleware.auth_token' under the 'oslo.config.opts'
+    namespace.
+
+    The purpose of this is to allow tools like the Oslo sample config file
+    generator to discover the options exposed to users by this middleware.
+
+    :returns: a list of (group_name, opts) tuples
+    """
+    return [(g, copy.deepcopy(o)) for g, o in auth_token_opts]
diff --git a/keystonemiddleware-moon/keystonemiddleware/s3_token.py b/keystonemiddleware-moon/keystonemiddleware/s3_token.py
new file mode 100644 (file)
index 0000000..d56482f
--- /dev/null
@@ -0,0 +1,267 @@
+# Copyright 2012 OpenStack Foundation
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2011,2012 Akira YOSHIYAMA <akirayoshiyama@gmail.com>
+# 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.
+
+# This source code is based ./auth_token.py and ./ec2_token.py.
+# See them for their copyright.
+
+"""
+S3 Token Middleware
+
+This WSGI component:
+
+* Gets a request from the swift3 middleware with an S3 Authorization
+  access key.
+* Validates s3 token in Keystone.
+* Transforms the account name to AUTH_%(tenant_name).
+
+"""
+
+import logging
+import webob
+
+from oslo_serialization import jsonutils
+import requests
+import six
+from six.moves import urllib
+
+from keystonemiddleware.i18n import _, _LI
+
+
+PROTOCOL_NAME = 'S3 Token Authentication'
+
+
+# TODO(kun): remove it after oslo merge this.
+def _split_path(path, minsegs=1, maxsegs=None, rest_with_last=False):
+    """Validate and split the given HTTP request path.
+
+    **Examples**::
+
+        ['a'] = _split_path('/a')
+        ['a', None] = _split_path('/a', 1, 2)
+        ['a', 'c'] = _split_path('/a/c', 1, 2)
+        ['a', 'c', 'o/r'] = _split_path('/a/c/o/r', 1, 3, True)
+
+    :param path: HTTP Request path to be split
+    :param minsegs: Minimum number of segments to be extracted
+    :param maxsegs: Maximum number of segments to be extracted
+    :param rest_with_last: If True, trailing data will be returned as part
+                           of last segment.  If False, and there is
+                           trailing data, raises ValueError.
+    :returns: list of segments with a length of maxsegs (non-existent
+              segments will return as None)
+    :raises: ValueError if given an invalid path
+    """
+    if not maxsegs:
+        maxsegs = minsegs
+    if minsegs > maxsegs:
+        raise ValueError(_('minsegs > maxsegs: %(min)d > %(max)d)') %
+                         {'min': minsegs, 'max': maxsegs})
+    if rest_with_last:
+        segs = path.split('/', maxsegs)
+        minsegs += 1
+        maxsegs += 1
+        count = len(segs)
+        if (segs[0] or count < minsegs or count > maxsegs or
+                '' in segs[1:minsegs]):
+            raise ValueError(_('Invalid path: %s') % urllib.parse.quote(path))
+    else:
+        minsegs += 1
+        maxsegs += 1
+        segs = path.split('/', maxsegs)
+        count = len(segs)
+        if (segs[0] or count < minsegs or count > maxsegs + 1 or
+                '' in segs[1:minsegs] or
+                (count == maxsegs + 1 and segs[maxsegs])):
+            raise ValueError(_('Invalid path: %s') % urllib.parse.quote(path))
+    segs = segs[1:maxsegs]
+    segs.extend([None] * (maxsegs - 1 - len(segs)))
+    return segs
+
+
+class ServiceError(Exception):
+    pass
+
+
+class S3Token(object):
+    """Middleware that handles S3 authentication."""
+
+    def __init__(self, app, conf):
+        """Common initialization code."""
+        self._app = app
+        self._logger = logging.getLogger(conf.get('log_name', __name__))
+        self._logger.debug('Starting the %s component', PROTOCOL_NAME)
+        self._reseller_prefix = conf.get('reseller_prefix', 'AUTH_')
+        # where to find the auth service (we use this to validate tokens)
+
+        auth_host = conf.get('auth_host')
+        auth_port = int(conf.get('auth_port', 35357))
+        auth_protocol = conf.get('auth_protocol', 'https')
+
+        self._request_uri = '%s://%s:%s' % (auth_protocol, auth_host,
+                                            auth_port)
+
+        # SSL
+        insecure = conf.get('insecure', False)
+        cert_file = conf.get('certfile')
+        key_file = conf.get('keyfile')
+
+        if insecure:
+            self._verify = False
+        elif cert_file and key_file:
+            self._verify = (cert_file, key_file)
+        elif cert_file:
+            self._verify = cert_file
+        else:
+            self._verify = None
+
+    def _deny_request(self, code):
+        error_table = {
+            'AccessDenied': (401, 'Access denied'),
+            'InvalidURI': (400, 'Could not parse the specified URI'),
+        }
+        resp = webob.Response(content_type='text/xml')
+        resp.status = error_table[code][0]
+        error_msg = ('<?xml version="1.0" encoding="UTF-8"?>\r\n'
+                     '<Error>\r\n  <Code>%s</Code>\r\n  '
+                     '<Message>%s</Message>\r\n</Error>\r\n' %
+                     (code, error_table[code][1]))
+        if six.PY3:
+            error_msg = error_msg.encode()
+        resp.body = error_msg
+        return resp
+
+    def _json_request(self, creds_json):
+        headers = {'Content-Type': 'application/json'}
+        try:
+            response = requests.post('%s/v2.0/s3tokens' % self._request_uri,
+                                     headers=headers, data=creds_json,
+                                     verify=self._verify)
+        except requests.exceptions.RequestException as e:
+            self._logger.info(_LI('HTTP connection exception: %s'), e)
+            resp = self._deny_request('InvalidURI')
+            raise ServiceError(resp)
+
+        if response.status_code < 200 or response.status_code >= 300:
+            self._logger.debug('Keystone reply error: status=%s reason=%s',
+                               response.status_code, response.reason)
+            resp = self._deny_request('AccessDenied')
+            raise ServiceError(resp)
+
+        return response
+
+    def __call__(self, environ, start_response):
+        """Handle incoming request. authenticate and send downstream."""
+        req = webob.Request(environ)
+        self._logger.debug('Calling S3Token middleware.')
+
+        try:
+            parts = _split_path(req.path, 1, 4, True)
+            version, account, container, obj = parts
+        except ValueError:
+            msg = 'Not a path query, skipping.'
+            self._logger.debug(msg)
+            return self._app(environ, start_response)
+
+        # Read request signature and access id.
+        if 'Authorization' not in req.headers:
+            msg = 'No Authorization header. skipping.'
+            self._logger.debug(msg)
+            return self._app(environ, start_response)
+
+        token = req.headers.get('X-Auth-Token',
+                                req.headers.get('X-Storage-Token'))
+        if not token:
+            msg = 'You did not specify an auth or a storage token. skipping.'
+            self._logger.debug(msg)
+            return self._app(environ, start_response)
+
+        auth_header = req.headers['Authorization']
+        try:
+            access, signature = auth_header.split(' ')[-1].rsplit(':', 1)
+        except ValueError:
+            msg = 'You have an invalid Authorization header: %s'
+            self._logger.debug(msg, auth_header)
+            return self._deny_request('InvalidURI')(environ, start_response)
+
+        # NOTE(chmou): This is to handle the special case with nova
+        # when we have the option s3_affix_tenant. We will force it to
+        # connect to another account than the one
+        # authenticated. Before people start getting worried about
+        # security, I should point that we are connecting with
+        # username/token specified by the user but instead of
+        # connecting to its own account we will force it to go to an
+        # another account. In a normal scenario if that user don't
+        # have the reseller right it will just fail but since the
+        # reseller account can connect to every account it is allowed
+        # by the swift_auth middleware.
+        force_tenant = None
+        if ':' in access:
+            access, force_tenant = access.split(':')
+
+        # Authenticate request.
+        creds = {'credentials': {'access': access,
+                                 'token': token,
+                                 'signature': signature}}
+        creds_json = jsonutils.dumps(creds)
+        self._logger.debug('Connecting to Keystone sending this JSON: %s',
+                           creds_json)
+        # NOTE(vish): We could save a call to keystone by having
+        #             keystone return token, tenant, user, and roles
+        #             from this call.
+        #
+        # NOTE(chmou): We still have the same problem we would need to
+        #              change token_auth to detect if we already
+        #              identified and not doing a second query and just
+        #              pass it through to swiftauth in this case.
+        try:
+            resp = self._json_request(creds_json)
+        except ServiceError as e:
+            resp = e.args[0]
+            msg = 'Received error, exiting middleware with error: %s'
+            self._logger.debug(msg, resp.status_code)
+            return resp(environ, start_response)
+
+        self._logger.debug('Keystone Reply: Status: %d, Output: %s',
+                           resp.status_code, resp.content)
+
+        try:
+            identity_info = resp.json()
+            token_id = str(identity_info['access']['token']['id'])
+            tenant = identity_info['access']['token']['tenant']
+        except (ValueError, KeyError):
+            error = 'Error on keystone reply: %d %s'
+            self._logger.debug(error, resp.status_code, resp.content)
+            return self._deny_request('InvalidURI')(environ, start_response)
+
+        req.headers['X-Auth-Token'] = token_id
+        tenant_to_connect = force_tenant or tenant['id']
+        self._logger.debug('Connecting with tenant: %s', tenant_to_connect)
+        new_tenant_name = '%s%s' % (self._reseller_prefix, tenant_to_connect)
+        environ['PATH_INFO'] = environ['PATH_INFO'].replace(account,
+                                                            new_tenant_name)
+        return self._app(environ, start_response)
+
+
+def filter_factory(global_conf, **local_conf):
+    """Returns a WSGI filter app for use with paste.deploy."""
+    conf = global_conf.copy()
+    conf.update(local_conf)
+
+    def auth_filter(app):
+        return S3Token(app, conf)
+    return auth_filter
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/__init__.py b/keystonemiddleware-moon/keystonemiddleware/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/__init__.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/__init__.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_auth.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_auth.py
new file mode 100644 (file)
index 0000000..517d597
--- /dev/null
@@ -0,0 +1,102 @@
+# 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.
+
+import logging
+import uuid
+
+from keystoneclient import auth
+from keystoneclient import fixture
+from keystoneclient import session
+from requests_mock.contrib import fixture as rm_fixture
+import six
+import testtools
+
+from keystonemiddleware.auth_token import _auth
+
+
+class DefaultAuthPluginTests(testtools.TestCase):
+
+    def new_plugin(self, auth_host=None, auth_port=None, auth_protocol=None,
+                   auth_admin_prefix=None, admin_user=None,
+                   admin_password=None, admin_tenant_name=None,
+                   admin_token=None, identity_uri=None, log=None):
+        if not log:
+            log = self.logger
+
+        return _auth.AuthTokenPlugin.load_from_options(
+            auth_host=auth_host,
+            auth_port=auth_port,
+            auth_protocol=auth_protocol,
+            auth_admin_prefix=auth_admin_prefix,
+            admin_user=admin_user,
+            admin_password=admin_password,
+            admin_tenant_name=admin_tenant_name,
+            admin_token=admin_token,
+            identity_uri=identity_uri,
+            log=log)
+
+    def setUp(self):
+        super(DefaultAuthPluginTests, self).setUp()
+
+        self.stream = six.StringIO()
+        self.logger = logging.getLogger(__name__)
+        self.session = session.Session()
+        self.requests = self.useFixture(rm_fixture.Fixture())
+
+    def test_auth_uri_from_fragments(self):
+        auth_protocol = 'http'
+        auth_host = 'testhost'
+        auth_port = 8888
+        auth_admin_prefix = 'admin'
+
+        expected = '%s://%s:%d/admin' % (auth_protocol, auth_host, auth_port)
+
+        plugin = self.new_plugin(auth_host=auth_host,
+                                 auth_protocol=auth_protocol,
+                                 auth_port=auth_port,
+                                 auth_admin_prefix=auth_admin_prefix)
+
+        self.assertEqual(expected,
+                         plugin.get_endpoint(self.session,
+                                             interface=auth.AUTH_INTERFACE))
+
+    def test_identity_uri_overrides_fragments(self):
+        identity_uri = 'http://testhost:8888/admin'
+        plugin = self.new_plugin(identity_uri=identity_uri,
+                                 auth_host='anotherhost',
+                                 auth_port=9999,
+                                 auth_protocol='ftp')
+
+        self.assertEqual(identity_uri,
+                         plugin.get_endpoint(self.session,
+                                             interface=auth.AUTH_INTERFACE))
+
+    def test_with_admin_token(self):
+        token = uuid.uuid4().hex
+        plugin = self.new_plugin(identity_uri='http://testhost:8888/admin',
+                                 admin_token=token)
+        self.assertEqual(token, plugin.get_token(self.session))
+
+    def test_with_user_pass(self):
+        base_uri = 'http://testhost:8888/admin'
+        token = fixture.V2Token()
+        admin_tenant_name = uuid.uuid4().hex
+
+        self.requests.post(base_uri + '/v2.0/tokens',
+                           json=token)
+
+        plugin = self.new_plugin(identity_uri=base_uri,
+                                 admin_user=uuid.uuid4().hex,
+                                 admin_password=uuid.uuid4().hex,
+                                 admin_tenant_name=admin_tenant_name)
+
+        self.assertEqual(token.token_id, plugin.get_token(self.session))
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py
new file mode 100644 (file)
index 0000000..97fcc55
--- /dev/null
@@ -0,0 +1,2763 @@
+# Copyright 2012 OpenStack Foundation
+#
+# 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.
+
+import calendar
+import datetime
+import json
+import logging
+import os
+import shutil
+import stat
+import tempfile
+import time
+import uuid
+
+import fixtures
+from keystoneclient import access
+from keystoneclient import auth
+from keystoneclient.common import cms
+from keystoneclient import exceptions
+from keystoneclient import fixture
+from keystoneclient import session
+import mock
+from oslo_config import fixture as cfg_fixture
+from oslo_serialization import jsonutils
+from oslo_utils import timeutils
+from requests_mock.contrib import fixture as rm_fixture
+import six
+import testresources
+import testtools
+from testtools import matchers
+import webob
+import webob.dec
+
+from keystonemiddleware import auth_token
+from keystonemiddleware.auth_token import _base
+from keystonemiddleware.auth_token import _exceptions as exc
+from keystonemiddleware.auth_token import _revocations
+from keystonemiddleware.openstack.common import memorycache
+from keystonemiddleware.tests.unit import client_fixtures
+from keystonemiddleware.tests.unit import utils
+
+
+EXPECTED_V2_DEFAULT_ENV_RESPONSE = {
+    'HTTP_X_IDENTITY_STATUS': 'Confirmed',
+    'HTTP_X_TENANT_ID': 'tenant_id1',
+    'HTTP_X_TENANT_NAME': 'tenant_name1',
+    'HTTP_X_USER_ID': 'user_id1',
+    'HTTP_X_USER_NAME': 'user_name1',
+    'HTTP_X_ROLES': 'role1,role2',
+    'HTTP_X_USER': 'user_name1',  # deprecated (diablo-compat)
+    'HTTP_X_TENANT': 'tenant_name1',  # deprecated (diablo-compat)
+    'HTTP_X_ROLE': 'role1,role2',  # deprecated (diablo-compat)
+}
+
+EXPECTED_V2_DEFAULT_SERVICE_ENV_RESPONSE = {
+    'HTTP_X_SERVICE_IDENTITY_STATUS': 'Confirmed',
+    'HTTP_X_SERVICE_PROJECT_ID': 'service_project_id1',
+    'HTTP_X_SERVICE_PROJECT_NAME': 'service_project_name1',
+    'HTTP_X_SERVICE_USER_ID': 'service_user_id1',
+    'HTTP_X_SERVICE_USER_NAME': 'service_user_name1',
+    'HTTP_X_SERVICE_ROLES': 'service_role1,service_role2',
+}
+
+EXPECTED_V3_DEFAULT_ENV_ADDITIONS = {
+    'HTTP_X_PROJECT_DOMAIN_ID': 'domain_id1',
+    'HTTP_X_PROJECT_DOMAIN_NAME': 'domain_name1',
+    'HTTP_X_USER_DOMAIN_ID': 'domain_id1',
+    'HTTP_X_USER_DOMAIN_NAME': 'domain_name1',
+}
+
+EXPECTED_V3_DEFAULT_SERVICE_ENV_ADDITIONS = {
+    'HTTP_X_SERVICE_PROJECT_DOMAIN_ID': 'service_domain_id1',
+    'HTTP_X_SERVICE_PROJECT_DOMAIN_NAME': 'service_domain_name1',
+    'HTTP_X_SERVICE_USER_DOMAIN_ID': 'service_domain_id1',
+    'HTTP_X_SERVICE_USER_DOMAIN_NAME': 'service_domain_name1'
+}
+
+
+BASE_HOST = 'https://keystone.example.com:1234'
+BASE_URI = '%s/testadmin' % BASE_HOST
+FAKE_ADMIN_TOKEN_ID = 'admin_token2'
+FAKE_ADMIN_TOKEN = jsonutils.dumps(
+    {'access': {'token': {'id': FAKE_ADMIN_TOKEN_ID,
+                          'expires': '2022-10-03T16:58:01Z'}}})
+
+VERSION_LIST_v3 = fixture.DiscoveryList(href=BASE_URI)
+VERSION_LIST_v2 = fixture.DiscoveryList(v3=False, href=BASE_URI)
+
+ERROR_TOKEN = '7ae290c2a06244c4b41692eb4e9225f2'
+MEMCACHED_SERVERS = ['localhost:11211']
+MEMCACHED_AVAILABLE = None
+
+
+def memcached_available():
+    """Do a sanity check against memcached.
+
+    Returns ``True`` if the following conditions are met (otherwise, returns
+    ``False``):
+
+    - ``python-memcached`` is installed
+    - a usable ``memcached`` instance is available via ``MEMCACHED_SERVERS``
+    - the client is able to set and get a key/value pair
+
+    """
+    global MEMCACHED_AVAILABLE
+
+    if MEMCACHED_AVAILABLE is None:
+        try:
+            import memcache
+            c = memcache.Client(MEMCACHED_SERVERS)
+            c.set('ping', 'pong', time=1)
+            MEMCACHED_AVAILABLE = c.get('ping') == 'pong'
+        except ImportError:
+            MEMCACHED_AVAILABLE = False
+
+    return MEMCACHED_AVAILABLE
+
+
+def cleanup_revoked_file(filename):
+    try:
+        os.remove(filename)
+    except OSError:
+        pass
+
+
+class TimezoneFixture(fixtures.Fixture):
+    @staticmethod
+    def supported():
+        # tzset is only supported on Unix.
+        return hasattr(time, 'tzset')
+
+    def __init__(self, new_tz):
+        super(TimezoneFixture, self).__init__()
+        self.tz = new_tz
+        self.old_tz = os.environ.get('TZ')
+
+    def setUp(self):
+        super(TimezoneFixture, self).setUp()
+        if not self.supported():
+            raise NotImplementedError('timezone override is not supported.')
+        os.environ['TZ'] = self.tz
+        time.tzset()
+        self.addCleanup(self.cleanup)
+
+    def cleanup(self):
+        if self.old_tz is not None:
+            os.environ['TZ'] = self.old_tz
+        elif 'TZ' in os.environ:
+            del os.environ['TZ']
+        time.tzset()
+
+
+class TimeFixture(fixtures.Fixture):
+
+    def __init__(self, new_time, normalize=True):
+        super(TimeFixture, self).__init__()
+        if isinstance(new_time, six.string_types):
+            new_time = timeutils.parse_isotime(new_time)
+        if normalize:
+            new_time = timeutils.normalize_time(new_time)
+        self.new_time = new_time
+
+    def setUp(self):
+        super(TimeFixture, self).setUp()
+        timeutils.set_time_override(self.new_time)
+        self.addCleanup(timeutils.clear_time_override)
+
+
+class FakeApp(object):
+    """This represents a WSGI app protected by the auth_token middleware."""
+
+    SUCCESS = b'SUCCESS'
+    FORBIDDEN = b'FORBIDDEN'
+    expected_env = {}
+
+    def __init__(self, expected_env=None, need_service_token=False):
+        self.expected_env = dict(EXPECTED_V2_DEFAULT_ENV_RESPONSE)
+
+        if expected_env:
+            self.expected_env.update(expected_env)
+
+        self.need_service_token = need_service_token
+
+    def __call__(self, env, start_response):
+        for k, v in self.expected_env.items():
+            assert env[k] == v, '%s != %s' % (env[k], v)
+
+        resp = webob.Response()
+
+        if (env.get('HTTP_X_IDENTITY_STATUS') == 'Invalid'
+                and env['HTTP_X_SERVICE_IDENTITY_STATUS'] == 'Invalid'):
+            # Simulate delayed auth forbidding access with arbitrary status
+            # code to differentiate checking this code path
+            resp.status = 419
+            resp.body = FakeApp.FORBIDDEN
+        elif env.get('HTTP_X_SERVICE_IDENTITY_STATUS') == 'Invalid':
+            # Simulate delayed auth forbidding access with arbitrary status
+            # code to differentiate checking this code path
+            resp.status = 420
+            resp.body = FakeApp.FORBIDDEN
+        elif env['HTTP_X_IDENTITY_STATUS'] == 'Invalid':
+            # Simulate delayed auth forbidding access
+            resp.status = 403
+            resp.body = FakeApp.FORBIDDEN
+        elif (self.need_service_token is True and
+                env.get('HTTP_X_SERVICE_TOKEN') is None):
+            # Simulate requiring composite auth
+            # Arbitrary value to allow checking this code path
+            resp.status = 418
+            resp.body = FakeApp.FORBIDDEN
+        else:
+            resp.body = FakeApp.SUCCESS
+
+        return resp(env, start_response)
+
+
+class v3FakeApp(FakeApp):
+    """This represents a v3 WSGI app protected by the auth_token middleware."""
+
+    def __init__(self, expected_env=None, need_service_token=False):
+
+        # with v3 additions, these are for the DEFAULT TOKEN
+        v3_default_env_additions = dict(EXPECTED_V3_DEFAULT_ENV_ADDITIONS)
+        if expected_env:
+            v3_default_env_additions.update(expected_env)
+        super(v3FakeApp, self).__init__(expected_env=v3_default_env_additions,
+                                        need_service_token=need_service_token)
+
+
+class CompositeBase(object):
+    """Base composite auth object with common service token environment."""
+
+    def __init__(self, expected_env=None):
+        comp_expected_env = dict(EXPECTED_V2_DEFAULT_SERVICE_ENV_RESPONSE)
+
+        if expected_env:
+            comp_expected_env.update(expected_env)
+
+        super(CompositeBase, self).__init__(
+            expected_env=comp_expected_env, need_service_token=True)
+
+
+class CompositeFakeApp(CompositeBase, FakeApp):
+    """A fake v2 WSGI app protected by composite auth_token middleware."""
+
+    def __init__(self, expected_env):
+        super(CompositeFakeApp, self).__init__(expected_env=expected_env)
+
+
+class v3CompositeFakeApp(CompositeBase, v3FakeApp):
+    """A fake v3 WSGI app protected by composite auth_token middleware."""
+
+    def __init__(self, expected_env=None):
+
+        # with v3 additions, these are for the DEFAULT SERVICE TOKEN
+        v3_default_service_env_additions = dict(
+            EXPECTED_V3_DEFAULT_SERVICE_ENV_ADDITIONS)
+
+        if expected_env:
+            v3_default_service_env_additions.update(expected_env)
+
+        super(v3CompositeFakeApp, self).__init__(
+            v3_default_service_env_additions)
+
+
+def new_app(status, body, headers={}):
+
+    class _App(object):
+
+        def __init__(self, expected_env=None):
+            self.expected_env = expected_env
+
+        @webob.dec.wsgify
+        def __call__(self, req):
+            resp = webob.Response(body, status)
+            resp.headers.update(headers)
+            return resp
+
+    return _App
+
+
+class BaseAuthTokenMiddlewareTest(testtools.TestCase):
+    """Base test class for auth_token middleware.
+
+    All the tests allow for running with auth_token
+    configured for receiving v2 or v3 tokens, with the
+    choice being made by passing configuration data into
+    setUp().
+
+    The base class will, by default, run all the tests
+    expecting v2 token formats.  Child classes can override
+    this to specify, for instance, v3 format.
+
+    """
+    def setUp(self, expected_env=None, auth_version=None, fake_app=None):
+        super(BaseAuthTokenMiddlewareTest, self).setUp()
+
+        self.expected_env = expected_env or dict()
+        self.fake_app = fake_app or FakeApp
+        self.middleware = None
+        self.requests = self.useFixture(rm_fixture.Fixture())
+
+        signing_dir = self._setup_signing_directory()
+
+        self.conf = {
+            'identity_uri': 'https://keystone.example.com:1234/testadmin/',
+            'signing_dir': signing_dir,
+            'auth_version': auth_version,
+            'auth_uri': 'https://keystone.example.com:1234',
+            'admin_user': uuid.uuid4().hex,
+        }
+
+        self.auth_version = auth_version
+        self.response_status = None
+        self.response_headers = None
+
+    def _setup_signing_directory(self):
+        directory_name = self.useFixture(fixtures.TempDir()).path
+
+        # Copy the sample certificate files into the temporary directory.
+        for filename in ['cacert.pem', 'signing_cert.pem', ]:
+            shutil.copy2(os.path.join(client_fixtures.CERTDIR, filename),
+                         os.path.join(directory_name, filename))
+
+        return directory_name
+
+    def set_middleware(self, expected_env=None, conf=None):
+        """Configure the class ready to call the auth_token middleware.
+
+        Set up the various fake items needed to run the middleware.
+        Individual tests that need to further refine these can call this
+        function to override the class defaults.
+
+        """
+        if conf:
+            self.conf.update(conf)
+
+        if expected_env:
+            self.expected_env.update(expected_env)
+
+        self.middleware = auth_token.AuthProtocol(
+            self.fake_app(self.expected_env), self.conf)
+
+        self.middleware._revocations._list = jsonutils.dumps(
+            {"revoked": [], "extra": "success"})
+
+    def update_expected_env(self, expected_env={}):
+        self.middleware._app.expected_env.update(expected_env)
+
+    def purge_token_expected_env(self):
+        for key in six.iterkeys(self.token_expected_env):
+            del self.middleware._app.expected_env[key]
+
+    def purge_service_token_expected_env(self):
+        for key in six.iterkeys(self.service_token_expected_env):
+            del self.middleware._app.expected_env[key]
+
+    def start_fake_response(self, status, headers, exc_info=None):
+        self.response_status = int(status.split(' ', 1)[0])
+        self.response_headers = dict(headers)
+
+    def assertLastPath(self, path):
+        if path:
+            self.assertEqual(BASE_URI + path, self.requests.last_request.url)
+        else:
+            self.assertIsNone(self.requests.last_request)
+
+
+class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+                                    testresources.ResourcedTestCase):
+
+    resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+    """Auth Token middleware should understand Diablo keystone responses."""
+    def setUp(self):
+        # pre-diablo only had Tenant ID, which was also the Name
+        expected_env = {
+            'HTTP_X_TENANT_ID': 'tenant_id1',
+            'HTTP_X_TENANT_NAME': 'tenant_id1',
+            # now deprecated (diablo-compat)
+            'HTTP_X_TENANT': 'tenant_id1',
+        }
+
+        super(DiabloAuthTokenMiddlewareTest, self).setUp(
+            expected_env=expected_env)
+
+        self.requests.get(BASE_URI,
+                          json=VERSION_LIST_v2,
+                          status_code=300)
+
+        self.requests.post("%s/v2.0/tokens" % BASE_URI,
+                           text=FAKE_ADMIN_TOKEN)
+
+        self.token_id = self.examples.VALID_DIABLO_TOKEN
+        token_response = self.examples.JSON_TOKEN_RESPONSES[self.token_id]
+
+        url = "%s/v2.0/tokens/%s" % (BASE_URI, self.token_id)
+        self.requests.get(url, text=token_response)
+
+        self.set_middleware()
+
+    def test_valid_diablo_response(self):
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = self.token_id
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 200)
+        self.assertIn('keystone.token_info', req.environ)
+
+
+class NoMemcacheAuthToken(BaseAuthTokenMiddlewareTest):
+    """These tests will not have the memcache module available."""
+
+    def setUp(self):
+        super(NoMemcacheAuthToken, self).setUp()
+        self.useFixture(utils.DisableModuleFixture('memcache'))
+
+    def test_nomemcache(self):
+        conf = {
+            'admin_token': 'admin_token1',
+            'auth_host': 'keystone.example.com',
+            'auth_port': '1234',
+            'memcached_servers': ','.join(MEMCACHED_SERVERS),
+            'auth_uri': 'https://keystone.example.com:1234',
+        }
+
+        auth_token.AuthProtocol(FakeApp(), conf)
+
+
+class CachePoolTest(BaseAuthTokenMiddlewareTest):
+    def test_use_cache_from_env(self):
+        """If `swift.cache` is set in the environment and `cache` is set in the
+        config then the env cache is used.
+        """
+        env = {'swift.cache': 'CACHE_TEST'}
+        conf = {
+            'cache': 'swift.cache'
+        }
+        self.set_middleware(conf=conf)
+        self.middleware._token_cache.initialize(env)
+        with self.middleware._token_cache._cache_pool.reserve() as cache:
+            self.assertEqual(cache, 'CACHE_TEST')
+
+    def test_not_use_cache_from_env(self):
+        """If `swift.cache` is set in the environment but `cache` isn't set in
+        the config then the env cache isn't used.
+        """
+        self.set_middleware()
+        env = {'swift.cache': 'CACHE_TEST'}
+        self.middleware._token_cache.initialize(env)
+        with self.middleware._token_cache._cache_pool.reserve() as cache:
+            self.assertNotEqual(cache, 'CACHE_TEST')
+
+    def test_multiple_context_managers_share_single_client(self):
+        self.set_middleware()
+        token_cache = self.middleware._token_cache
+        env = {}
+        token_cache.initialize(env)
+
+        caches = []
+
+        with token_cache._cache_pool.reserve() as cache:
+            caches.append(cache)
+
+        with token_cache._cache_pool.reserve() as cache:
+            caches.append(cache)
+
+        self.assertIs(caches[0], caches[1])
+        self.assertEqual(set(caches), set(token_cache._cache_pool))
+
+    def test_nested_context_managers_create_multiple_clients(self):
+        self.set_middleware()
+        env = {}
+        self.middleware._token_cache.initialize(env)
+        token_cache = self.middleware._token_cache
+
+        with token_cache._cache_pool.reserve() as outer_cache:
+            with token_cache._cache_pool.reserve() as inner_cache:
+                self.assertNotEqual(outer_cache, inner_cache)
+
+        self.assertEqual(
+            set([inner_cache, outer_cache]),
+            set(token_cache._cache_pool))
+
+
+class GeneralAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+                                     testresources.ResourcedTestCase):
+    """These tests are not affected by the token format
+    (see CommonAuthTokenMiddlewareTest).
+    """
+
+    resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+    def test_token_is_v2_accepts_v2(self):
+        token = self.examples.UUID_TOKEN_DEFAULT
+        token_response = self.examples.TOKEN_RESPONSES[token]
+        self.assertTrue(auth_token._token_is_v2(token_response))
+
+    def test_token_is_v2_rejects_v3(self):
+        token = self.examples.v3_UUID_TOKEN_DEFAULT
+        token_response = self.examples.TOKEN_RESPONSES[token]
+        self.assertFalse(auth_token._token_is_v2(token_response))
+
+    def test_token_is_v3_rejects_v2(self):
+        token = self.examples.UUID_TOKEN_DEFAULT
+        token_response = self.examples.TOKEN_RESPONSES[token]
+        self.assertFalse(auth_token._token_is_v3(token_response))
+
+    def test_token_is_v3_accepts_v3(self):
+        token = self.examples.v3_UUID_TOKEN_DEFAULT
+        token_response = self.examples.TOKEN_RESPONSES[token]
+        self.assertTrue(auth_token._token_is_v3(token_response))
+
+    @testtools.skipUnless(memcached_available(), 'memcached not available')
+    def test_encrypt_cache_data(self):
+        conf = {
+            'memcached_servers': ','.join(MEMCACHED_SERVERS),
+            'memcache_security_strategy': 'encrypt',
+            'memcache_secret_key': 'mysecret'
+        }
+        self.set_middleware(conf=conf)
+        token = b'my_token'
+        some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4)
+        expires = timeutils.strtime(some_time_later)
+        data = ('this_data', expires)
+        token_cache = self.middleware._token_cache
+        token_cache.initialize({})
+        token_cache._cache_store(token, data)
+        self.assertEqual(token_cache._cache_get(token), data[0])
+
+    @testtools.skipUnless(memcached_available(), 'memcached not available')
+    def test_sign_cache_data(self):
+        conf = {
+            'memcached_servers': ','.join(MEMCACHED_SERVERS),
+            'memcache_security_strategy': 'mac',
+            'memcache_secret_key': 'mysecret'
+        }
+        self.set_middleware(conf=conf)
+        token = b'my_token'
+        some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4)
+        expires = timeutils.strtime(some_time_later)
+        data = ('this_data', expires)
+        token_cache = self.middleware._token_cache
+        token_cache.initialize({})
+        token_cache._cache_store(token, data)
+        self.assertEqual(token_cache._cache_get(token), data[0])
+
+    @testtools.skipUnless(memcached_available(), 'memcached not available')
+    def test_no_memcache_protection(self):
+        conf = {
+            'memcached_servers': ','.join(MEMCACHED_SERVERS),
+            'memcache_secret_key': 'mysecret'
+        }
+        self.set_middleware(conf=conf)
+        token = 'my_token'
+        some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4)
+        expires = timeutils.strtime(some_time_later)
+        data = ('this_data', expires)
+        token_cache = self.middleware._token_cache
+        token_cache.initialize({})
+        token_cache._cache_store(token, data)
+        self.assertEqual(token_cache._cache_get(token), data[0])
+
+    def test_assert_valid_memcache_protection_config(self):
+        # test missing memcache_secret_key
+        conf = {
+            'memcached_servers': ','.join(MEMCACHED_SERVERS),
+            'memcache_security_strategy': 'Encrypt'
+        }
+        self.assertRaises(exc.ConfigurationError, self.set_middleware,
+                          conf=conf)
+        # test invalue memcache_security_strategy
+        conf = {
+            'memcached_servers': ','.join(MEMCACHED_SERVERS),
+            'memcache_security_strategy': 'whatever'
+        }
+        self.assertRaises(exc.ConfigurationError, self.set_middleware,
+                          conf=conf)
+        # test missing memcache_secret_key
+        conf = {
+            'memcached_servers': ','.join(MEMCACHED_SERVERS),
+            'memcache_security_strategy': 'mac'
+        }
+        self.assertRaises(exc.ConfigurationError, self.set_middleware,
+                          conf=conf)
+        conf = {
+            'memcached_servers': ','.join(MEMCACHED_SERVERS),
+            'memcache_security_strategy': 'Encrypt',
+            'memcache_secret_key': ''
+        }
+        self.assertRaises(exc.ConfigurationError, self.set_middleware,
+                          conf=conf)
+        conf = {
+            'memcached_servers': ','.join(MEMCACHED_SERVERS),
+            'memcache_security_strategy': 'mAc',
+            'memcache_secret_key': ''
+        }
+        self.assertRaises(exc.ConfigurationError, self.set_middleware,
+                          conf=conf)
+
+    def test_config_revocation_cache_timeout(self):
+        conf = {
+            'revocation_cache_time': '24',
+            'auth_uri': 'https://keystone.example.com:1234',
+            'admin_user': uuid.uuid4().hex
+        }
+        middleware = auth_token.AuthProtocol(self.fake_app, conf)
+        self.assertEqual(middleware._revocations._cache_timeout,
+                         datetime.timedelta(seconds=24))
+
+    def test_conf_values_type_convert(self):
+        conf = {
+            'revocation_cache_time': '24',
+            'identity_uri': 'https://keystone.example.com:1234',
+            'include_service_catalog': '0',
+            'nonexsit_option': '0',
+        }
+
+        middleware = auth_token.AuthProtocol(self.fake_app, conf)
+        self.assertEqual(datetime.timedelta(seconds=24),
+                         middleware._revocations._cache_timeout)
+        self.assertEqual(False, middleware._include_service_catalog)
+        self.assertEqual('0', middleware._conf['nonexsit_option'])
+
+    def test_deprecated_conf_values(self):
+        conf = {
+            'memcache_servers': ','.join(MEMCACHED_SERVERS),
+        }
+
+        middleware = auth_token.AuthProtocol(self.fake_app, conf)
+        self.assertEqual(MEMCACHED_SERVERS,
+                         middleware._conf_get('memcached_servers'))
+
+    def test_conf_values_type_convert_with_wrong_value(self):
+        conf = {
+            'include_service_catalog': '123',
+        }
+        self.assertRaises(exc.ConfigurationError,
+                          auth_token.AuthProtocol, self.fake_app, conf)
+
+
+class CommonAuthTokenMiddlewareTest(object):
+    """These tests are run once using v2 tokens and again using v3 tokens."""
+
+    def test_init_does_not_call_http(self):
+        conf = {
+            'revocation_cache_time': '1'
+        }
+        self.set_middleware(conf=conf)
+        self.assertLastPath(None)
+
+    def test_auth_with_no_token_does_not_call_http(self):
+        self.set_middleware()
+        req = webob.Request.blank('/')
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertLastPath(None)
+        self.assertEqual(401, self.response_status)
+
+    def test_init_by_ipv6Addr_auth_host(self):
+        del self.conf['identity_uri']
+        conf = {
+            'auth_host': '2001:2013:1:f101::1',
+            'auth_port': '1234',
+            'auth_protocol': 'http',
+            'auth_uri': None,
+            'auth_version': 'v3.0',
+        }
+        self.set_middleware(conf=conf)
+        expected_auth_uri = 'http://[2001:2013:1:f101::1]:1234'
+        self.assertEqual(expected_auth_uri,
+                         self.middleware._auth_uri)
+
+    def assert_valid_request_200(self, token, with_catalog=True):
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = token
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 200)
+        if with_catalog:
+            self.assertTrue(req.headers.get('X-Service-Catalog'))
+        else:
+            self.assertNotIn('X-Service-Catalog', req.headers)
+        self.assertEqual(body, [FakeApp.SUCCESS])
+        self.assertIn('keystone.token_info', req.environ)
+        return req
+
+    def test_valid_uuid_request(self):
+        for _ in range(2):  # Do it twice because first result was cached.
+            token = self.token_dict['uuid_token_default']
+            self.assert_valid_request_200(token)
+            self.assert_valid_last_url(token)
+
+    def test_valid_uuid_request_with_auth_fragments(self):
+        del self.conf['identity_uri']
+        self.conf['auth_protocol'] = 'https'
+        self.conf['auth_host'] = 'keystone.example.com'
+        self.conf['auth_port'] = '1234'
+        self.conf['auth_admin_prefix'] = '/testadmin'
+        self.set_middleware()
+        self.assert_valid_request_200(self.token_dict['uuid_token_default'])
+        self.assert_valid_last_url(self.token_dict['uuid_token_default'])
+
+    def _test_cache_revoked(self, token, revoked_form=None):
+        # When the token is cached and revoked, 401 is returned.
+        self.middleware._check_revocations_for_cached = True
+
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = token
+
+        # Token should be cached as ok after this.
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(200, self.response_status)
+
+        # Put it in revocation list.
+        self.middleware._revocations._list = self.get_revocation_list_json(
+            token_ids=[revoked_form or token])
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(401, self.response_status)
+
+    def test_cached_revoked_uuid(self):
+        # When the UUID token is cached and revoked, 401 is returned.
+        self._test_cache_revoked(self.token_dict['uuid_token_default'])
+
+    def test_valid_signed_request(self):
+        for _ in range(2):  # Do it twice because first result was cached.
+            self.assert_valid_request_200(
+                self.token_dict['signed_token_scoped'])
+            # ensure that signed requests do not generate HTTP traffic
+            self.assertLastPath(None)
+
+    def test_valid_signed_compressed_request(self):
+        self.assert_valid_request_200(
+            self.token_dict['signed_token_scoped_pkiz'])
+        # ensure that signed requests do not generate HTTP traffic
+        self.assertLastPath(None)
+
+    def test_revoked_token_receives_401(self):
+        self.middleware._revocations._list = (
+            self.get_revocation_list_json())
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = self.token_dict['revoked_token']
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 401)
+
+    def test_revoked_token_receives_401_sha256(self):
+        self.conf['hash_algorithms'] = ','.join(['sha256', 'md5'])
+        self.set_middleware()
+        self.middleware._revocations._list = (
+            self.get_revocation_list_json(mode='sha256'))
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = self.token_dict['revoked_token']
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 401)
+
+    def test_cached_revoked_pki(self):
+        # When the PKI token is cached and revoked, 401 is returned.
+        token = self.token_dict['signed_token_scoped']
+        revoked_form = cms.cms_hash_token(token)
+        self._test_cache_revoked(token, revoked_form)
+
+    def test_cached_revoked_pkiz(self):
+        # When the PKIZ token is cached and revoked, 401 is returned.
+        token = self.token_dict['signed_token_scoped_pkiz']
+        revoked_form = cms.cms_hash_token(token)
+        self._test_cache_revoked(token, revoked_form)
+
+    def test_revoked_token_receives_401_md5_secondary(self):
+        # When hash_algorithms has 'md5' as the secondary hash and the
+        # revocation list contains the md5 hash for a token, that token is
+        # considered revoked so returns 401.
+        self.conf['hash_algorithms'] = ','.join(['sha256', 'md5'])
+        self.set_middleware()
+        self.middleware._revocations._list = (
+            self.get_revocation_list_json())
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = self.token_dict['revoked_token']
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 401)
+
+    def _test_revoked_hashed_token(self, token_name):
+        # If hash_algorithms is set as ['sha256', 'md5'],
+        # and check_revocations_for_cached is True,
+        # and a token is in the cache because it was successfully validated
+        # using the md5 hash, then
+        # if the token is in the revocation list by md5 hash, it'll be
+        # rejected and auth_token returns 401.
+        self.conf['hash_algorithms'] = ','.join(['sha256', 'md5'])
+        self.conf['check_revocations_for_cached'] = 'true'
+        self.set_middleware()
+
+        token = self.token_dict[token_name]
+
+        # Put the token in the revocation list.
+        token_hashed = cms.cms_hash_token(token)
+        self.middleware._revocations._list = self.get_revocation_list_json(
+            token_ids=[token_hashed])
+
+        # First, request is using the hashed token, is valid so goes in
+        # cache using the given hash.
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = token_hashed
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(200, self.response_status)
+
+        # This time use the PKI(Z) token
+        req.headers['X-Auth-Token'] = token
+        self.middleware(req.environ, self.start_fake_response)
+
+        # Should find the token in the cache and revocation list.
+        self.assertEqual(401, self.response_status)
+
+    def test_revoked_hashed_pki_token(self):
+        self._test_revoked_hashed_token('signed_token_scoped')
+
+    def test_revoked_hashed_pkiz_token(self):
+        self._test_revoked_hashed_token('signed_token_scoped_pkiz')
+
+    def get_revocation_list_json(self, token_ids=None, mode=None):
+        if token_ids is None:
+            key = 'revoked_token_hash' + (('_' + mode) if mode else '')
+            token_ids = [self.token_dict[key]]
+        revocation_list = {'revoked': [{'id': x, 'expires': timeutils.utcnow()}
+                                       for x in token_ids]}
+        return jsonutils.dumps(revocation_list)
+
+    def test_is_signed_token_revoked_returns_false(self):
+        # explicitly setting an empty revocation list here to document intent
+        self.middleware._revocations._list = jsonutils.dumps(
+            {"revoked": [], "extra": "success"})
+        result = self.middleware._revocations._any_revoked(
+            [self.token_dict['revoked_token_hash']])
+        self.assertFalse(result)
+
+    def test_is_signed_token_revoked_returns_true(self):
+        self.middleware._revocations._list = (
+            self.get_revocation_list_json())
+        result = self.middleware._revocations._any_revoked(
+            [self.token_dict['revoked_token_hash']])
+        self.assertTrue(result)
+
+    def test_is_signed_token_revoked_returns_true_sha256(self):
+        self.conf['hash_algorithms'] = ','.join(['sha256', 'md5'])
+        self.set_middleware()
+        self.middleware._revocations._list = (
+            self.get_revocation_list_json(mode='sha256'))
+        result = self.middleware._revocations._any_revoked(
+            [self.token_dict['revoked_token_hash_sha256']])
+        self.assertTrue(result)
+
+    def test_verify_signed_token_raises_exception_for_revoked_token(self):
+        self.middleware._revocations._list = (
+            self.get_revocation_list_json())
+        self.assertRaises(exc.InvalidToken,
+                          self.middleware._verify_signed_token,
+                          self.token_dict['revoked_token'],
+                          [self.token_dict['revoked_token_hash']])
+
+    def test_verify_signed_token_raises_exception_for_revoked_token_s256(self):
+        self.conf['hash_algorithms'] = ','.join(['sha256', 'md5'])
+        self.set_middleware()
+        self.middleware._revocations._list = (
+            self.get_revocation_list_json(mode='sha256'))
+        self.assertRaises(exc.InvalidToken,
+                          self.middleware._verify_signed_token,
+                          self.token_dict['revoked_token'],
+                          [self.token_dict['revoked_token_hash_sha256'],
+                           self.token_dict['revoked_token_hash']])
+
+    def test_verify_signed_token_raises_exception_for_revoked_pkiz_token(self):
+        self.middleware._revocations._list = (
+            self.examples.REVOKED_TOKEN_PKIZ_LIST_JSON)
+        self.assertRaises(exc.InvalidToken,
+                          self.middleware._verify_pkiz_token,
+                          self.token_dict['revoked_token_pkiz'],
+                          [self.token_dict['revoked_token_pkiz_hash']])
+
+    def assertIsValidJSON(self, text):
+        json.loads(text)
+
+    def test_verify_signed_token_succeeds_for_unrevoked_token(self):
+        self.middleware._revocations._list = (
+            self.get_revocation_list_json())
+        text = self.middleware._verify_signed_token(
+            self.token_dict['signed_token_scoped'],
+            [self.token_dict['signed_token_scoped_hash']])
+        self.assertIsValidJSON(text)
+
+    def test_verify_signed_compressed_token_succeeds_for_unrevoked_token(self):
+        self.middleware._revocations._list = (
+            self.get_revocation_list_json())
+        text = self.middleware._verify_pkiz_token(
+            self.token_dict['signed_token_scoped_pkiz'],
+            [self.token_dict['signed_token_scoped_hash']])
+        self.assertIsValidJSON(text)
+
+    def test_verify_signed_token_succeeds_for_unrevoked_token_sha256(self):
+        self.conf['hash_algorithms'] = ','.join(['sha256', 'md5'])
+        self.set_middleware()
+        self.middleware._revocations._list = (
+            self.get_revocation_list_json(mode='sha256'))
+        text = self.middleware._verify_signed_token(
+            self.token_dict['signed_token_scoped'],
+            [self.token_dict['signed_token_scoped_hash_sha256'],
+             self.token_dict['signed_token_scoped_hash']])
+        self.assertIsValidJSON(text)
+
+    def test_get_token_revocation_list_fetched_time_returns_min(self):
+        self.middleware._revocations._fetched_time = None
+
+        # Get rid of the revoked file
+        revoked_path = self.middleware._signing_directory.calc_path(
+            _revocations.Revocations._FILE_NAME)
+        os.remove(revoked_path)
+
+        self.assertEqual(self.middleware._revocations._fetched_time,
+                         datetime.datetime.min)
+
+    # FIXME(blk-u): move the unit tests into unit/test_auth_token.py
+    def test_get_token_revocation_list_fetched_time_returns_mtime(self):
+        self.middleware._revocations._fetched_time = None
+        revoked_path = self.middleware._signing_directory.calc_path(
+            _revocations.Revocations._FILE_NAME)
+        mtime = os.path.getmtime(revoked_path)
+        fetched_time = datetime.datetime.utcfromtimestamp(mtime)
+        self.assertEqual(fetched_time,
+                         self.middleware._revocations._fetched_time)
+
+    @testtools.skipUnless(TimezoneFixture.supported(),
+                          'TimezoneFixture not supported')
+    def test_get_token_revocation_list_fetched_time_returns_utc(self):
+        with TimezoneFixture('UTC-1'):
+            self.middleware._revocations._list = jsonutils.dumps(
+                self.examples.REVOCATION_LIST)
+            self.middleware._revocations._fetched_time = None
+            fetched_time = self.middleware._revocations._fetched_time
+            self.assertTrue(timeutils.is_soon(fetched_time, 1))
+
+    def test_get_token_revocation_list_fetched_time_returns_value(self):
+        expected = self.middleware._revocations._fetched_time
+        self.assertEqual(self.middleware._revocations._fetched_time,
+                         expected)
+
+    def test_get_revocation_list_returns_fetched_list(self):
+        # auth_token uses v2 to fetch this, so don't allow the v3
+        # tests to override the fake http connection
+        self.middleware._revocations._fetched_time = None
+
+        # Get rid of the revoked file
+        revoked_path = self.middleware._signing_directory.calc_path(
+            _revocations.Revocations._FILE_NAME)
+        os.remove(revoked_path)
+
+        self.assertEqual(self.middleware._revocations._list,
+                         self.examples.REVOCATION_LIST)
+
+    def test_get_revocation_list_returns_current_list_from_memory(self):
+        self.assertEqual(self.middleware._revocations._list,
+                         self.middleware._revocations._list_prop)
+
+    def test_get_revocation_list_returns_current_list_from_disk(self):
+        in_memory_list = self.middleware._revocations._list
+        self.middleware._revocations._list_prop = None
+        self.assertEqual(self.middleware._revocations._list,
+                         in_memory_list)
+
+    def test_invalid_revocation_list_raises_error(self):
+        self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI, json={})
+
+        self.assertRaises(exc.RevocationListError,
+                          self.middleware._revocations._fetch)
+
+    def test_fetch_revocation_list(self):
+        # auth_token uses v2 to fetch this, so don't allow the v3
+        # tests to override the fake http connection
+        fetched = jsonutils.loads(self.middleware._revocations._fetch())
+        self.assertEqual(fetched, self.examples.REVOCATION_LIST)
+
+    def test_request_invalid_uuid_token(self):
+        # remember because we are testing the middleware we stub the connection
+        # to the keystone server, but this is not what gets returned
+        invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI
+        self.requests.get(invalid_uri, status_code=404)
+
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = 'invalid-token'
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 401)
+        self.assertEqual(self.response_headers['WWW-Authenticate'],
+                         "Keystone uri='https://keystone.example.com:1234'")
+
+    def test_request_invalid_signed_token(self):
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = self.examples.INVALID_SIGNED_TOKEN
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(401, self.response_status)
+        self.assertEqual("Keystone uri='https://keystone.example.com:1234'",
+                         self.response_headers['WWW-Authenticate'])
+
+    def test_request_invalid_signed_pkiz_token(self):
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = self.examples.INVALID_SIGNED_PKIZ_TOKEN
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(401, self.response_status)
+        self.assertEqual("Keystone uri='https://keystone.example.com:1234'",
+                         self.response_headers['WWW-Authenticate'])
+
+    def test_request_no_token(self):
+        req = webob.Request.blank('/')
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 401)
+        self.assertEqual(self.response_headers['WWW-Authenticate'],
+                         "Keystone uri='https://keystone.example.com:1234'")
+
+    def test_request_no_token_log_message(self):
+        class FakeLog(object):
+            def __init__(self):
+                self.msg = None
+                self.debugmsg = None
+
+            def warn(self, msg=None, *args, **kwargs):
+                self.msg = msg
+
+            def debug(self, msg=None, *args, **kwargs):
+                self.debugmsg = msg
+
+        self.middleware._LOG = FakeLog()
+        self.middleware._delay_auth_decision = False
+        self.assertRaises(exc.InvalidToken,
+                          self.middleware._get_user_token_from_header, {})
+        self.assertIsNotNone(self.middleware._LOG.msg)
+        self.assertIsNotNone(self.middleware._LOG.debugmsg)
+
+    def test_request_no_token_http(self):
+        req = webob.Request.blank('/', environ={'REQUEST_METHOD': 'HEAD'})
+        self.set_middleware()
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 401)
+        self.assertEqual(self.response_headers['WWW-Authenticate'],
+                         "Keystone uri='https://keystone.example.com:1234'")
+        self.assertEqual(body, [''])
+
+    def test_request_blank_token(self):
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = ''
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 401)
+        self.assertEqual(self.response_headers['WWW-Authenticate'],
+                         "Keystone uri='https://keystone.example.com:1234'")
+
+    def _get_cached_token(self, token, mode='md5'):
+        token_id = cms.cms_hash_token(token, mode=mode)
+        return self.middleware._token_cache._cache_get(token_id)
+
+    def test_memcache(self):
+        req = webob.Request.blank('/')
+        token = self.token_dict['signed_token_scoped']
+        req.headers['X-Auth-Token'] = token
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertIsNotNone(self._get_cached_token(token))
+
+    def test_expired(self):
+        req = webob.Request.blank('/')
+        token = self.token_dict['signed_token_scoped_expired']
+        req.headers['X-Auth-Token'] = token
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 401)
+
+    def test_memcache_set_invalid_uuid(self):
+        invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI
+        self.requests.get(invalid_uri, status_code=404)
+
+        req = webob.Request.blank('/')
+        token = 'invalid-token'
+        req.headers['X-Auth-Token'] = token
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertRaises(exc.InvalidToken,
+                          self._get_cached_token, token)
+
+    def _test_memcache_set_invalid_signed(self, hash_algorithms=None,
+                                          exp_mode='md5'):
+        req = webob.Request.blank('/')
+        token = self.token_dict['signed_token_scoped_expired']
+        req.headers['X-Auth-Token'] = token
+        if hash_algorithms:
+            self.conf['hash_algorithms'] = ','.join(hash_algorithms)
+            self.set_middleware()
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertRaises(exc.InvalidToken,
+                          self._get_cached_token, token, mode=exp_mode)
+
+    def test_memcache_set_invalid_signed(self):
+        self._test_memcache_set_invalid_signed()
+
+    def test_memcache_set_invalid_signed_sha256_md5(self):
+        hash_algorithms = ['sha256', 'md5']
+        self._test_memcache_set_invalid_signed(hash_algorithms=hash_algorithms,
+                                               exp_mode='sha256')
+
+    def test_memcache_set_invalid_signed_sha256(self):
+        hash_algorithms = ['sha256']
+        self._test_memcache_set_invalid_signed(hash_algorithms=hash_algorithms,
+                                               exp_mode='sha256')
+
+    def test_memcache_set_expired(self, extra_conf={}, extra_environ={}):
+        token_cache_time = 10
+        conf = {
+            'token_cache_time': '%s' % token_cache_time,
+        }
+        conf.update(extra_conf)
+        self.set_middleware(conf=conf)
+        req = webob.Request.blank('/')
+        token = self.token_dict['signed_token_scoped']
+        req.headers['X-Auth-Token'] = token
+        req.environ.update(extra_environ)
+
+        now = datetime.datetime.utcnow()
+        self.useFixture(TimeFixture(now))
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertIsNotNone(self._get_cached_token(token))
+
+        timeutils.advance_time_seconds(token_cache_time)
+        self.assertIsNone(self._get_cached_token(token))
+
+    def test_swift_memcache_set_expired(self):
+        extra_conf = {'cache': 'swift.cache'}
+        extra_environ = {'swift.cache': memorycache.Client()}
+        self.test_memcache_set_expired(extra_conf, extra_environ)
+
+    def test_http_error_not_cached_token(self):
+        """Test to don't cache token as invalid on network errors.
+
+        We use UUID tokens since they are the easiest one to reach
+        get_http_connection.
+        """
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = ERROR_TOKEN
+        self.middleware._http_request_max_retries = 0
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertIsNone(self._get_cached_token(ERROR_TOKEN))
+        self.assert_valid_last_url(ERROR_TOKEN)
+
+    def test_http_request_max_retries(self):
+        times_retry = 10
+
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = ERROR_TOKEN
+
+        conf = {'http_request_max_retries': '%s' % times_retry}
+        self.set_middleware(conf=conf)
+
+        with mock.patch('time.sleep') as mock_obj:
+            self.middleware(req.environ, self.start_fake_response)
+
+        self.assertEqual(mock_obj.call_count, times_retry)
+
+    def test_nocatalog(self):
+        conf = {
+            'include_service_catalog': 'False'
+        }
+        self.set_middleware(conf=conf)
+        self.assert_valid_request_200(self.token_dict['uuid_token_default'],
+                                      with_catalog=False)
+
+    def assert_kerberos_bind(self, token, bind_level,
+                             use_kerberos=True, success=True):
+        conf = {
+            'enforce_token_bind': bind_level,
+            'auth_version': self.auth_version,
+        }
+        self.set_middleware(conf=conf)
+
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = token
+
+        if use_kerberos:
+            if use_kerberos is True:
+                req.environ['REMOTE_USER'] = self.examples.KERBEROS_BIND
+            else:
+                req.environ['REMOTE_USER'] = use_kerberos
+
+            req.environ['AUTH_TYPE'] = 'Negotiate'
+
+        body = self.middleware(req.environ, self.start_fake_response)
+
+        if success:
+            self.assertEqual(self.response_status, 200)
+            self.assertEqual(body, [FakeApp.SUCCESS])
+            self.assertIn('keystone.token_info', req.environ)
+            self.assert_valid_last_url(token)
+        else:
+            self.assertEqual(self.response_status, 401)
+            self.assertEqual(self.response_headers['WWW-Authenticate'],
+                             "Keystone uri='https://keystone.example.com:1234'"
+                             )
+
+    def test_uuid_bind_token_disabled_with_kerb_user(self):
+        for use_kerberos in [True, False]:
+            self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                      bind_level='disabled',
+                                      use_kerberos=use_kerberos,
+                                      success=True)
+
+    def test_uuid_bind_token_disabled_with_incorrect_ticket(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='kerberos',
+                                  use_kerberos='ronald@MCDONALDS.COM',
+                                  success=False)
+
+    def test_uuid_bind_token_permissive_with_kerb_user(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='permissive',
+                                  use_kerberos=True,
+                                  success=True)
+
+    def test_uuid_bind_token_permissive_without_kerb_user(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='permissive',
+                                  use_kerberos=False,
+                                  success=False)
+
+    def test_uuid_bind_token_permissive_with_unknown_bind(self):
+        token = self.token_dict['uuid_token_unknown_bind']
+
+        for use_kerberos in [True, False]:
+            self.assert_kerberos_bind(token,
+                                      bind_level='permissive',
+                                      use_kerberos=use_kerberos,
+                                      success=True)
+
+    def test_uuid_bind_token_permissive_with_incorrect_ticket(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='kerberos',
+                                  use_kerberos='ronald@MCDONALDS.COM',
+                                  success=False)
+
+    def test_uuid_bind_token_strict_with_kerb_user(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='strict',
+                                  use_kerberos=True,
+                                  success=True)
+
+    def test_uuid_bind_token_strict_with_kerbout_user(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='strict',
+                                  use_kerberos=False,
+                                  success=False)
+
+    def test_uuid_bind_token_strict_with_unknown_bind(self):
+        token = self.token_dict['uuid_token_unknown_bind']
+
+        for use_kerberos in [True, False]:
+            self.assert_kerberos_bind(token,
+                                      bind_level='strict',
+                                      use_kerberos=use_kerberos,
+                                      success=False)
+
+    def test_uuid_bind_token_required_with_kerb_user(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='required',
+                                  use_kerberos=True,
+                                  success=True)
+
+    def test_uuid_bind_token_required_without_kerb_user(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='required',
+                                  use_kerberos=False,
+                                  success=False)
+
+    def test_uuid_bind_token_required_with_unknown_bind(self):
+        token = self.token_dict['uuid_token_unknown_bind']
+
+        for use_kerberos in [True, False]:
+            self.assert_kerberos_bind(token,
+                                      bind_level='required',
+                                      use_kerberos=use_kerberos,
+                                      success=False)
+
+    def test_uuid_bind_token_required_without_bind(self):
+        for use_kerberos in [True, False]:
+            self.assert_kerberos_bind(self.token_dict['uuid_token_default'],
+                                      bind_level='required',
+                                      use_kerberos=use_kerberos,
+                                      success=False)
+
+    def test_uuid_bind_token_named_kerberos_with_kerb_user(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='kerberos',
+                                  use_kerberos=True,
+                                  success=True)
+
+    def test_uuid_bind_token_named_kerberos_without_kerb_user(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='kerberos',
+                                  use_kerberos=False,
+                                  success=False)
+
+    def test_uuid_bind_token_named_kerberos_with_unknown_bind(self):
+        token = self.token_dict['uuid_token_unknown_bind']
+
+        for use_kerberos in [True, False]:
+            self.assert_kerberos_bind(token,
+                                      bind_level='kerberos',
+                                      use_kerberos=use_kerberos,
+                                      success=False)
+
+    def test_uuid_bind_token_named_kerberos_without_bind(self):
+        for use_kerberos in [True, False]:
+            self.assert_kerberos_bind(self.token_dict['uuid_token_default'],
+                                      bind_level='kerberos',
+                                      use_kerberos=use_kerberos,
+                                      success=False)
+
+    def test_uuid_bind_token_named_kerberos_with_incorrect_ticket(self):
+        self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+                                  bind_level='kerberos',
+                                  use_kerberos='ronald@MCDONALDS.COM',
+                                  success=False)
+
+    def test_uuid_bind_token_with_unknown_named_FOO(self):
+        token = self.token_dict['uuid_token_bind']
+
+        for use_kerberos in [True, False]:
+            self.assert_kerberos_bind(token,
+                                      bind_level='FOO',
+                                      use_kerberos=use_kerberos,
+                                      success=False)
+
+    def test_caching_token_on_verify(self):
+        # When the token is cached it isn't cached again when it's verified.
+
+        # The token cache has to be initialized with our cache instance.
+        self.middleware._token_cache._env_cache_name = 'cache'
+        cache = memorycache.Client()
+        self.middleware._token_cache.initialize(env={'cache': cache})
+
+        # Mock cache.set since then the test can verify call_count.
+        orig_cache_set = cache.set
+        cache.set = mock.Mock(side_effect=orig_cache_set)
+
+        token = self.token_dict['signed_token_scoped']
+
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = token
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(200, self.response_status)
+
+        self.assertThat(1, matchers.Equals(cache.set.call_count))
+
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = token
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(200, self.response_status)
+
+        # Assert that the token wasn't cached again.
+        self.assertThat(1, matchers.Equals(cache.set.call_count))
+
+    def test_auth_plugin(self):
+
+        for service_url in (self.examples.UNVERSIONED_SERVICE_URL,
+                            self.examples.SERVICE_URL):
+            self.requests.get(service_url,
+                              json=VERSION_LIST_v3,
+                              status_code=300)
+
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = self.token_dict['uuid_token_default']
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(200, self.response_status)
+        self.assertEqual([FakeApp.SUCCESS], body)
+
+        token_auth = req.environ['keystone.token_auth']
+        endpoint_filter = {'service_type': self.examples.SERVICE_TYPE,
+                           'version': 3}
+
+        url = token_auth.get_endpoint(session.Session(), **endpoint_filter)
+        self.assertEqual('%s/v3' % BASE_URI, url)
+
+        self.assertTrue(token_auth.has_user_token)
+        self.assertFalse(token_auth.has_service_token)
+        self.assertIsNone(token_auth.service)
+
+
+class V2CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest,
+                                   testresources.ResourcedTestCase):
+
+    resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+    def __init__(self, *args, **kwargs):
+        super(V2CertDownloadMiddlewareTest, self).__init__(*args, **kwargs)
+        self.auth_version = 'v2.0'
+        self.fake_app = None
+        self.ca_path = '/v2.0/certificates/ca'
+        self.signing_path = '/v2.0/certificates/signing'
+
+    def setUp(self):
+        super(V2CertDownloadMiddlewareTest, self).setUp(
+            auth_version=self.auth_version,
+            fake_app=self.fake_app)
+        self.base_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.base_dir)
+        self.cert_dir = os.path.join(self.base_dir, 'certs')
+        os.makedirs(self.cert_dir, stat.S_IRWXU)
+        conf = {
+            'signing_dir': self.cert_dir,
+            'auth_version': self.auth_version,
+        }
+
+        self.requests.register_uri('GET',
+                                   BASE_URI,
+                                   json=VERSION_LIST_v3,
+                                   status_code=300)
+
+        self.set_middleware(conf=conf)
+
+    # Usually we supply a signed_dir with pre-installed certificates,
+    # so invocation of /usr/bin/openssl succeeds. This time we give it
+    # an empty directory, so it fails.
+    def test_request_no_token_dummy(self):
+        cms._ensure_subprocess()
+
+        self.requests.get('%s%s' % (BASE_URI, self.ca_path),
+                          status_code=404)
+        self.requests.get('%s%s' % (BASE_URI, self.signing_path),
+                          status_code=404)
+        self.assertRaises(exceptions.CertificateConfigError,
+                          self.middleware._verify_signed_token,
+                          self.examples.SIGNED_TOKEN_SCOPED,
+                          [self.examples.SIGNED_TOKEN_SCOPED_HASH])
+
+    def test_fetch_signing_cert(self):
+        data = 'FAKE CERT'
+        url = "%s%s" % (BASE_URI, self.signing_path)
+        self.requests.get(url, text=data)
+        self.middleware._fetch_signing_cert()
+
+        signing_cert_path = self.middleware._signing_directory.calc_path(
+            self.middleware._SIGNING_CERT_FILE_NAME)
+        with open(signing_cert_path, 'r') as f:
+            self.assertEqual(f.read(), data)
+
+        self.assertEqual(url, self.requests.last_request.url)
+
+    def test_fetch_signing_ca(self):
+        data = 'FAKE CA'
+        url = "%s%s" % (BASE_URI, self.ca_path)
+        self.requests.get(url, text=data)
+        self.middleware._fetch_ca_cert()
+
+        ca_file_path = self.middleware._signing_directory.calc_path(
+            self.middleware._SIGNING_CA_FILE_NAME)
+        with open(ca_file_path, 'r') as f:
+            self.assertEqual(f.read(), data)
+
+        self.assertEqual(url, self.requests.last_request.url)
+
+    def test_prefix_trailing_slash(self):
+        del self.conf['identity_uri']
+        self.conf['auth_protocol'] = 'https'
+        self.conf['auth_host'] = 'keystone.example.com'
+        self.conf['auth_port'] = '1234'
+        self.conf['auth_admin_prefix'] = '/newadmin/'
+
+        base_url = '%s/newadmin' % BASE_HOST
+        ca_url = "%s%s" % (base_url, self.ca_path)
+        signing_url = "%s%s" % (base_url, self.signing_path)
+
+        self.requests.get(base_url,
+                          json=VERSION_LIST_v3,
+                          status_code=300)
+        self.requests.get(ca_url, text='FAKECA')
+        self.requests.get(signing_url, text='FAKECERT')
+
+        self.set_middleware(conf=self.conf)
+
+        self.middleware._fetch_ca_cert()
+        self.assertEqual(ca_url, self.requests.last_request.url)
+
+        self.middleware._fetch_signing_cert()
+        self.assertEqual(signing_url, self.requests.last_request.url)
+
+    def test_without_prefix(self):
+        del self.conf['identity_uri']
+        self.conf['auth_protocol'] = 'https'
+        self.conf['auth_host'] = 'keystone.example.com'
+        self.conf['auth_port'] = '1234'
+        self.conf['auth_admin_prefix'] = ''
+
+        ca_url = "%s%s" % (BASE_HOST, self.ca_path)
+        signing_url = "%s%s" % (BASE_HOST, self.signing_path)
+
+        self.requests.get(BASE_HOST,
+                          json=VERSION_LIST_v3,
+                          status_code=300)
+        self.requests.get(ca_url, text='FAKECA')
+        self.requests.get(signing_url, text='FAKECERT')
+
+        self.set_middleware(conf=self.conf)
+
+        self.middleware._fetch_ca_cert()
+        self.assertEqual(ca_url, self.requests.last_request.url)
+
+        self.middleware._fetch_signing_cert()
+        self.assertEqual(signing_url, self.requests.last_request.url)
+
+
+class V3CertDownloadMiddlewareTest(V2CertDownloadMiddlewareTest):
+
+    def __init__(self, *args, **kwargs):
+        super(V3CertDownloadMiddlewareTest, self).__init__(*args, **kwargs)
+        self.auth_version = 'v3.0'
+        self.fake_app = v3FakeApp
+        self.ca_path = '/v3/OS-SIMPLE-CERT/ca'
+        self.signing_path = '/v3/OS-SIMPLE-CERT/certificates'
+
+
+def network_error_response(request, context):
+    raise exceptions.ConnectionError("Network connection error.")
+
+
+class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+                                CommonAuthTokenMiddlewareTest,
+                                testresources.ResourcedTestCase):
+    """v2 token specific tests.
+
+    There are some differences between how the auth-token middleware handles
+    v2 and v3 tokens over and above the token formats, namely:
+
+    - A v3 keystone server will auto scope a token to a user's default project
+      if no scope is specified. A v2 server assumes that the auth-token
+      middleware will do that.
+    - A v2 keystone server may issue a token without a catalog, even with a
+      tenant
+
+    The tests below were originally part of the generic AuthTokenMiddlewareTest
+    class, but now, since they really are v2 specific, they are included here.
+
+    """
+
+    resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+    def setUp(self):
+        super(v2AuthTokenMiddlewareTest, self).setUp()
+
+        self.token_dict = {
+            'uuid_token_default': self.examples.UUID_TOKEN_DEFAULT,
+            'uuid_token_unscoped': self.examples.UUID_TOKEN_UNSCOPED,
+            'uuid_token_bind': self.examples.UUID_TOKEN_BIND,
+            'uuid_token_unknown_bind': self.examples.UUID_TOKEN_UNKNOWN_BIND,
+            'signed_token_scoped': self.examples.SIGNED_TOKEN_SCOPED,
+            'signed_token_scoped_pkiz': self.examples.SIGNED_TOKEN_SCOPED_PKIZ,
+            'signed_token_scoped_hash': self.examples.SIGNED_TOKEN_SCOPED_HASH,
+            'signed_token_scoped_hash_sha256':
+            self.examples.SIGNED_TOKEN_SCOPED_HASH_SHA256,
+            'signed_token_scoped_expired':
+            self.examples.SIGNED_TOKEN_SCOPED_EXPIRED,
+            'revoked_token': self.examples.REVOKED_TOKEN,
+            'revoked_token_pkiz': self.examples.REVOKED_TOKEN_PKIZ,
+            'revoked_token_pkiz_hash':
+            self.examples.REVOKED_TOKEN_PKIZ_HASH,
+            'revoked_token_hash': self.examples.REVOKED_TOKEN_HASH,
+            'revoked_token_hash_sha256':
+            self.examples.REVOKED_TOKEN_HASH_SHA256,
+        }
+
+        self.requests.get(BASE_URI,
+                          json=VERSION_LIST_v2,
+                          status_code=300)
+
+        self.requests.post('%s/v2.0/tokens' % BASE_URI,
+                           text=FAKE_ADMIN_TOKEN)
+
+        self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI,
+                          text=self.examples.SIGNED_REVOCATION_LIST)
+
+        for token in (self.examples.UUID_TOKEN_DEFAULT,
+                      self.examples.UUID_TOKEN_UNSCOPED,
+                      self.examples.UUID_TOKEN_BIND,
+                      self.examples.UUID_TOKEN_UNKNOWN_BIND,
+                      self.examples.UUID_TOKEN_NO_SERVICE_CATALOG,
+                      self.examples.SIGNED_TOKEN_SCOPED_KEY,
+                      self.examples.SIGNED_TOKEN_SCOPED_PKIZ_KEY,):
+            url = "%s/v2.0/tokens/%s" % (BASE_URI, token)
+            text = self.examples.JSON_TOKEN_RESPONSES[token]
+            self.requests.get(url, text=text)
+
+        url = '%s/v2.0/tokens/%s' % (BASE_URI, ERROR_TOKEN)
+        self.requests.get(url, text=network_error_response)
+
+        self.set_middleware()
+
+    def assert_unscoped_default_tenant_auto_scopes(self, token):
+        """Unscoped v2 requests with a default tenant should "auto-scope."
+
+        The implied scope is the user's tenant ID.
+
+        """
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = token
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 200)
+        self.assertEqual(body, [FakeApp.SUCCESS])
+        self.assertIn('keystone.token_info', req.environ)
+
+    def assert_valid_last_url(self, token_id):
+        self.assertLastPath("/v2.0/tokens/%s" % token_id)
+
+    def test_default_tenant_uuid_token(self):
+        self.assert_unscoped_default_tenant_auto_scopes(
+            self.examples.UUID_TOKEN_DEFAULT)
+
+    def test_default_tenant_signed_token(self):
+        self.assert_unscoped_default_tenant_auto_scopes(
+            self.examples.SIGNED_TOKEN_SCOPED)
+
+    def assert_unscoped_token_receives_401(self, token):
+        """Unscoped requests with no default tenant ID should be rejected."""
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = token
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 401)
+        self.assertEqual(self.response_headers['WWW-Authenticate'],
+                         "Keystone uri='https://keystone.example.com:1234'")
+
+    def test_unscoped_uuid_token_receives_401(self):
+        self.assert_unscoped_token_receives_401(
+            self.examples.UUID_TOKEN_UNSCOPED)
+
+    def test_unscoped_pki_token_receives_401(self):
+        self.assert_unscoped_token_receives_401(
+            self.examples.SIGNED_TOKEN_UNSCOPED)
+
+    def test_request_prevent_service_catalog_injection(self):
+        req = webob.Request.blank('/')
+        req.headers['X-Service-Catalog'] = '[]'
+        req.headers['X-Auth-Token'] = (
+            self.examples.UUID_TOKEN_NO_SERVICE_CATALOG)
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 200)
+        self.assertFalse(req.headers.get('X-Service-Catalog'))
+        self.assertEqual(body, [FakeApp.SUCCESS])
+
+    def test_user_plugin_token_properties(self):
+        req = webob.Request.blank('/')
+        req.headers['X-Service-Catalog'] = '[]'
+        token = self.examples.UUID_TOKEN_DEFAULT
+        token_data = self.examples.TOKEN_RESPONSES[token]
+        req.headers['X-Auth-Token'] = token
+        req.headers['X-Service-Token'] = token
+
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 200)
+        self.assertEqual([FakeApp.SUCCESS], body)
+
+        token_auth = req.environ['keystone.token_auth']
+
+        self.assertTrue(token_auth.has_user_token)
+        self.assertTrue(token_auth.has_service_token)
+
+        for t in [token_auth.user, token_auth.service]:
+            self.assertEqual(token_data.user_id, t.user_id)
+            self.assertEqual(token_data.tenant_id, t.project_id)
+
+            self.assertThat(t.role_names, matchers.HasLength(2))
+            self.assertIn('role1', t.role_names)
+            self.assertIn('role2', t.role_names)
+
+            self.assertIsNone(t.trust_id)
+            self.assertIsNone(t.user_domain_id)
+            self.assertIsNone(t.project_domain_id)
+
+
+class CrossVersionAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+                                          testresources.ResourcedTestCase):
+
+    resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+    def test_valid_uuid_request_forced_to_2_0(self):
+        """Test forcing auth_token to use lower api version.
+
+        By installing the v3 http hander, auth_token will be get
+        a version list that looks like a v3 server - from which it
+        would normally chose v3.0 as the auth version.  However, here
+        we specify v2.0 in the configuration - which should force
+        auth_token to use that version instead.
+
+        """
+        conf = {
+            'auth_version': 'v2.0'
+        }
+
+        self.requests.get(BASE_URI,
+                          json=VERSION_LIST_v3,
+                          status_code=300)
+
+        self.requests.post('%s/v2.0/tokens' % BASE_URI,
+                           text=FAKE_ADMIN_TOKEN)
+
+        token = self.examples.UUID_TOKEN_DEFAULT
+        url = "%s/v2.0/tokens/%s" % (BASE_URI, token)
+        text = self.examples.JSON_TOKEN_RESPONSES[token]
+        self.requests.get(url, text=text)
+
+        self.set_middleware(conf=conf)
+
+        # This tests will only work is auth_token has chosen to use the
+        # lower, v2, api version
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = self.examples.UUID_TOKEN_DEFAULT
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 200)
+        self.assertEqual(url, self.requests.last_request.url)
+
+
+class v3AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+                                CommonAuthTokenMiddlewareTest,
+                                testresources.ResourcedTestCase):
+    """Test auth_token middleware with v3 tokens.
+
+    Re-execute the AuthTokenMiddlewareTest class tests, but with the
+    auth_token middleware configured to expect v3 tokens back from
+    a keystone server.
+
+    This is done by configuring the AuthTokenMiddlewareTest class via
+    its Setup(), passing in v3 style data that will then be used by
+    the tests themselves.  This approach has been used to ensure we
+    really are running the same tests for both v2 and v3 tokens.
+
+    There a few additional specific test for v3 only:
+
+    - We allow an unscoped token to be validated (as unscoped), where
+      as for v2 tokens, the auth_token middleware is expected to try and
+      auto-scope it (and fail if there is no default tenant)
+    - Domain scoped tokens
+
+    Since we don't specify an auth version for auth_token to use, by
+    definition we are thefore implicitely testing that it will use
+    the highest available auth version, i.e. v3.0
+
+    """
+
+    resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+    def setUp(self):
+        super(v3AuthTokenMiddlewareTest, self).setUp(
+            auth_version='v3.0',
+            fake_app=v3FakeApp)
+
+        self.token_dict = {
+            'uuid_token_default': self.examples.v3_UUID_TOKEN_DEFAULT,
+            'uuid_token_unscoped': self.examples.v3_UUID_TOKEN_UNSCOPED,
+            'uuid_token_bind': self.examples.v3_UUID_TOKEN_BIND,
+            'uuid_token_unknown_bind':
+            self.examples.v3_UUID_TOKEN_UNKNOWN_BIND,
+            'signed_token_scoped': self.examples.SIGNED_v3_TOKEN_SCOPED,
+            'signed_token_scoped_pkiz':
+            self.examples.SIGNED_v3_TOKEN_SCOPED_PKIZ,
+            'signed_token_scoped_hash':
+            self.examples.SIGNED_v3_TOKEN_SCOPED_HASH,
+            'signed_token_scoped_hash_sha256':
+            self.examples.SIGNED_v3_TOKEN_SCOPED_HASH_SHA256,
+            'signed_token_scoped_expired':
+            self.examples.SIGNED_TOKEN_SCOPED_EXPIRED,
+            'revoked_token': self.examples.REVOKED_v3_TOKEN,
+            'revoked_token_pkiz': self.examples.REVOKED_v3_TOKEN_PKIZ,
+            'revoked_token_hash': self.examples.REVOKED_v3_TOKEN_HASH,
+            'revoked_token_hash_sha256':
+            self.examples.REVOKED_v3_TOKEN_HASH_SHA256,
+            'revoked_token_pkiz_hash':
+            self.examples.REVOKED_v3_PKIZ_TOKEN_HASH,
+        }
+
+        self.requests.get(BASE_URI,
+                          json=VERSION_LIST_v3,
+                          status_code=300)
+
+        # TODO(jamielennox): auth_token middleware uses a v2 admin token
+        # regardless of the auth_version that is set.
+        self.requests.post('%s/v2.0/tokens' % BASE_URI,
+                           text=FAKE_ADMIN_TOKEN)
+
+        # TODO(jamielennox): there is no v3 revocation url yet, it uses v2
+        self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI,
+                          text=self.examples.SIGNED_REVOCATION_LIST)
+
+        self.requests.get('%s/v3/auth/tokens' % BASE_URI,
+                          text=self.token_response)
+
+        self.set_middleware()
+
+    def token_response(self, request, context):
+        auth_id = request.headers.get('X-Auth-Token')
+        token_id = request.headers.get('X-Subject-Token')
+        self.assertEqual(auth_id, FAKE_ADMIN_TOKEN_ID)
+
+        if token_id == ERROR_TOKEN:
+            raise exceptions.ConnectionError("Network connection error.")
+
+        try:
+            response = self.examples.JSON_TOKEN_RESPONSES[token_id]
+        except KeyError:
+            response = ""
+            context.status_code = 404
+
+        return response
+
+    def assert_valid_last_url(self, token_id):
+        self.assertLastPath('/v3/auth/tokens')
+
+    def test_valid_unscoped_uuid_request(self):
+        # Remove items that won't be in an unscoped token
+        delta_expected_env = {
+            'HTTP_X_PROJECT_ID': None,
+            'HTTP_X_PROJECT_NAME': None,
+            'HTTP_X_PROJECT_DOMAIN_ID': None,
+            'HTTP_X_PROJECT_DOMAIN_NAME': None,
+            'HTTP_X_TENANT_ID': None,
+            'HTTP_X_TENANT_NAME': None,
+            'HTTP_X_ROLES': '',
+            'HTTP_X_TENANT': None,
+            'HTTP_X_ROLE': '',
+        }
+        self.set_middleware(expected_env=delta_expected_env)
+        self.assert_valid_request_200(self.examples.v3_UUID_TOKEN_UNSCOPED,
+                                      with_catalog=False)
+        self.assertLastPath('/v3/auth/tokens')
+
+    def test_domain_scoped_uuid_request(self):
+        # Modify items compared to default token for a domain scope
+        delta_expected_env = {
+            'HTTP_X_DOMAIN_ID': 'domain_id1',
+            'HTTP_X_DOMAIN_NAME': 'domain_name1',
+            'HTTP_X_PROJECT_ID': None,
+            'HTTP_X_PROJECT_NAME': None,
+            'HTTP_X_PROJECT_DOMAIN_ID': None,
+            'HTTP_X_PROJECT_DOMAIN_NAME': None,
+            'HTTP_X_TENANT_ID': None,
+            'HTTP_X_TENANT_NAME': None,
+            'HTTP_X_TENANT': None
+        }
+        self.set_middleware(expected_env=delta_expected_env)
+        self.assert_valid_request_200(
+            self.examples.v3_UUID_TOKEN_DOMAIN_SCOPED)
+        self.assertLastPath('/v3/auth/tokens')
+
+    def test_gives_v2_catalog(self):
+        self.set_middleware()
+        req = self.assert_valid_request_200(
+            self.examples.SIGNED_v3_TOKEN_SCOPED)
+
+        catalog = jsonutils.loads(req.headers['X-Service-Catalog'])
+
+        for service in catalog:
+            for endpoint in service['endpoints']:
+                # no point checking everything, just that it's in v2 format
+                self.assertIn('adminURL', endpoint)
+                self.assertIn('publicURL', endpoint)
+                self.assertIn('adminURL', endpoint)
+
+    def test_fallback_to_online_validation_with_signing_error(self):
+        self.requests.register_uri(
+            'GET',
+            '%s/v3/OS-SIMPLE-CERT/certificates' % BASE_URI,
+            status_code=404)
+        self.assert_valid_request_200(self.token_dict['signed_token_scoped'])
+        self.assert_valid_request_200(
+            self.token_dict['signed_token_scoped_pkiz'])
+
+    def test_fallback_to_online_validation_with_ca_error(self):
+        self.requests.register_uri('GET',
+                                   '%s/v3/OS-SIMPLE-CERT/ca' % BASE_URI,
+                                   status_code=404)
+        self.assert_valid_request_200(self.token_dict['signed_token_scoped'])
+        self.assert_valid_request_200(
+            self.token_dict['signed_token_scoped_pkiz'])
+
+    def test_fallback_to_online_validation_with_revocation_list_error(self):
+        self.requests.register_uri('GET',
+                                   '%s/v2.0/tokens/revoked' % BASE_URI,
+                                   status_code=404)
+        self.assert_valid_request_200(self.token_dict['signed_token_scoped'])
+        self.assert_valid_request_200(
+            self.token_dict['signed_token_scoped_pkiz'])
+
+    def test_user_plugin_token_properties(self):
+        req = webob.Request.blank('/')
+        req.headers['X-Service-Catalog'] = '[]'
+        token = self.examples.v3_UUID_TOKEN_DEFAULT
+        token_data = self.examples.TOKEN_RESPONSES[token]
+        req.headers['X-Auth-Token'] = token
+        req.headers['X-Service-Token'] = token
+
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 200)
+        self.assertEqual([FakeApp.SUCCESS], body)
+
+        token_auth = req.environ['keystone.token_auth']
+
+        self.assertTrue(token_auth.has_user_token)
+        self.assertTrue(token_auth.has_service_token)
+
+        for t in [token_auth.user, token_auth.service]:
+            self.assertEqual(token_data.user_id, t.user_id)
+            self.assertEqual(token_data.project_id, t.project_id)
+            self.assertEqual(token_data.user_domain_id, t.user_domain_id)
+            self.assertEqual(token_data.project_domain_id, t.project_domain_id)
+
+            self.assertThat(t.role_names, matchers.HasLength(2))
+            self.assertIn('role1', t.role_names)
+            self.assertIn('role2', t.role_names)
+
+            self.assertIsNone(t.trust_id)
+
+
+class TokenExpirationTest(BaseAuthTokenMiddlewareTest):
+    def setUp(self):
+        super(TokenExpirationTest, self).setUp()
+        self.now = timeutils.utcnow()
+        self.delta = datetime.timedelta(hours=1)
+        self.one_hour_ago = timeutils.isotime(self.now - self.delta,
+                                              subsecond=True)
+        self.one_hour_earlier = timeutils.isotime(self.now + self.delta,
+                                                  subsecond=True)
+
+    def create_v2_token_fixture(self, expires=None):
+        v2_fixture = {
+            'access': {
+                'token': {
+                    'id': 'blah',
+                    'expires': expires or self.one_hour_earlier,
+                    'tenant': {
+                        'id': 'tenant_id1',
+                        'name': 'tenant_name1',
+                    },
+                },
+                'user': {
+                    'id': 'user_id1',
+                    'name': 'user_name1',
+                    'roles': [
+                        {'name': 'role1'},
+                        {'name': 'role2'},
+                    ],
+                },
+                'serviceCatalog': {}
+            },
+        }
+
+        return v2_fixture
+
+    def create_v3_token_fixture(self, expires=None):
+
+        v3_fixture = {
+            'token': {
+                'expires_at': expires or self.one_hour_earlier,
+                'user': {
+                    'id': 'user_id1',
+                    'name': 'user_name1',
+                    'domain': {
+                        'id': 'domain_id1',
+                        'name': 'domain_name1'
+                    }
+                },
+                'project': {
+                    'id': 'tenant_id1',
+                    'name': 'tenant_name1',
+                    'domain': {
+                        'id': 'domain_id1',
+                        'name': 'domain_name1'
+                    }
+                },
+                'roles': [
+                    {'name': 'role1', 'id': 'Role1'},
+                    {'name': 'role2', 'id': 'Role2'},
+                ],
+                'catalog': {}
+            }
+        }
+
+        return v3_fixture
+
+    def test_no_data(self):
+        data = {}
+        self.assertRaises(exc.InvalidToken,
+                          auth_token._get_token_expiration,
+                          data)
+
+    def test_bad_data(self):
+        data = {'my_happy_token_dict': 'woo'}
+        self.assertRaises(exc.InvalidToken,
+                          auth_token._get_token_expiration,
+                          data)
+
+    def test_v2_token_get_token_expiration_return_isotime(self):
+        data = self.create_v2_token_fixture()
+        actual_expires = auth_token._get_token_expiration(data)
+        self.assertEqual(self.one_hour_earlier, actual_expires)
+
+    def test_v2_token_not_expired(self):
+        data = self.create_v2_token_fixture()
+        expected_expires = data['access']['token']['expires']
+        actual_expires = auth_token._get_token_expiration(data)
+        self.assertEqual(actual_expires, expected_expires)
+
+    def test_v2_token_expired(self):
+        data = self.create_v2_token_fixture(expires=self.one_hour_ago)
+        expires = auth_token._get_token_expiration(data)
+        self.assertRaises(exc.InvalidToken,
+                          auth_token._confirm_token_not_expired,
+                          expires)
+
+    def test_v2_token_with_timezone_offset_not_expired(self):
+        self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z'))
+        data = self.create_v2_token_fixture(
+            expires='2000-01-01T05:05:10.000123Z')
+        expected_expires = '2000-01-01T05:05:10.000123Z'
+        actual_expires = auth_token._get_token_expiration(data)
+        self.assertEqual(actual_expires, expected_expires)
+
+    def test_v2_token_with_timezone_offset_expired(self):
+        self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z'))
+        data = self.create_v2_token_fixture(
+            expires='1999-12-31T19:05:10Z')
+        expires = auth_token._get_token_expiration(data)
+        self.assertRaises(exc.InvalidToken,
+                          auth_token._confirm_token_not_expired,
+                          expires)
+
+    def test_v3_token_get_token_expiration_return_isotime(self):
+        data = self.create_v3_token_fixture()
+        actual_expires = auth_token._get_token_expiration(data)
+        self.assertEqual(self.one_hour_earlier, actual_expires)
+
+    def test_v3_token_not_expired(self):
+        data = self.create_v3_token_fixture()
+        expected_expires = data['token']['expires_at']
+        actual_expires = auth_token._get_token_expiration(data)
+        self.assertEqual(actual_expires, expected_expires)
+
+    def test_v3_token_expired(self):
+        data = self.create_v3_token_fixture(expires=self.one_hour_ago)
+        expires = auth_token._get_token_expiration(data)
+        self.assertRaises(exc.InvalidToken,
+                          auth_token._confirm_token_not_expired,
+                          expires)
+
+    def test_v3_token_with_timezone_offset_not_expired(self):
+        self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z'))
+        data = self.create_v3_token_fixture(
+            expires='2000-01-01T05:05:10.000123Z')
+        expected_expires = '2000-01-01T05:05:10.000123Z'
+
+        actual_expires = auth_token._get_token_expiration(data)
+        self.assertEqual(actual_expires, expected_expires)
+
+    def test_v3_token_with_timezone_offset_expired(self):
+        self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z'))
+        data = self.create_v3_token_fixture(
+            expires='1999-12-31T19:05:10Z')
+        expires = auth_token._get_token_expiration(data)
+        self.assertRaises(exc.InvalidToken,
+                          auth_token._confirm_token_not_expired,
+                          expires)
+
+    def test_cached_token_not_expired(self):
+        token = 'mytoken'
+        data = 'this_data'
+        self.set_middleware()
+        self.middleware._token_cache.initialize({})
+        some_time_later = timeutils.strtime(at=(self.now + self.delta))
+        expires = some_time_later
+        self.middleware._token_cache.store(token, data, expires)
+        self.assertEqual(self.middleware._token_cache._cache_get(token), data)
+
+    def test_cached_token_not_expired_with_old_style_nix_timestamp(self):
+        """Ensure we cannot retrieve a token from the cache.
+
+        Getting a token from the cache should return None when the token data
+        in the cache stores the expires time as a \*nix style timestamp.
+
+        """
+        token = 'mytoken'
+        data = 'this_data'
+        self.set_middleware()
+        token_cache = self.middleware._token_cache
+        token_cache.initialize({})
+        some_time_later = self.now + self.delta
+        # Store a unix timestamp in the cache.
+        expires = calendar.timegm(some_time_later.timetuple())
+        token_cache.store(token, data, expires)
+        self.assertIsNone(token_cache._cache_get(token))
+
+    def test_cached_token_expired(self):
+        token = 'mytoken'
+        data = 'this_data'
+        self.set_middleware()
+        self.middleware._token_cache.initialize({})
+        some_time_earlier = timeutils.strtime(at=(self.now - self.delta))
+        expires = some_time_earlier
+        self.middleware._token_cache.store(token, data, expires)
+        self.assertThat(lambda: self.middleware._token_cache._cache_get(token),
+                        matchers.raises(exc.InvalidToken))
+
+    def test_cached_token_with_timezone_offset_not_expired(self):
+        token = 'mytoken'
+        data = 'this_data'
+        self.set_middleware()
+        self.middleware._token_cache.initialize({})
+        timezone_offset = datetime.timedelta(hours=2)
+        some_time_later = self.now - timezone_offset + self.delta
+        expires = timeutils.strtime(some_time_later) + '-02:00'
+        self.middleware._token_cache.store(token, data, expires)
+        self.assertEqual(self.middleware._token_cache._cache_get(token), data)
+
+    def test_cached_token_with_timezone_offset_expired(self):
+        token = 'mytoken'
+        data = 'this_data'
+        self.set_middleware()
+        self.middleware._token_cache.initialize({})
+        timezone_offset = datetime.timedelta(hours=2)
+        some_time_earlier = self.now - timezone_offset - self.delta
+        expires = timeutils.strtime(some_time_earlier) + '-02:00'
+        self.middleware._token_cache.store(token, data, expires)
+        self.assertThat(lambda: self.middleware._token_cache._cache_get(token),
+                        matchers.raises(exc.InvalidToken))
+
+
+class CatalogConversionTests(BaseAuthTokenMiddlewareTest):
+
+    PUBLIC_URL = 'http://server:5000/v2.0'
+    ADMIN_URL = 'http://admin:35357/v2.0'
+    INTERNAL_URL = 'http://internal:5000/v2.0'
+
+    REGION_ONE = 'RegionOne'
+    REGION_TWO = 'RegionTwo'
+    REGION_THREE = 'RegionThree'
+
+    def test_basic_convert(self):
+        token = fixture.V3Token()
+        s = token.add_service(type='identity')
+        s.add_standard_endpoints(public=self.PUBLIC_URL,
+                                 admin=self.ADMIN_URL,
+                                 internal=self.INTERNAL_URL,
+                                 region=self.REGION_ONE)
+
+        auth_ref = access.AccessInfo.factory(body=token)
+        catalog_data = auth_ref.service_catalog.get_data()
+        catalog = auth_token._v3_to_v2_catalog(catalog_data)
+
+        self.assertEqual(1, len(catalog))
+        service = catalog[0]
+        self.assertEqual(1, len(service['endpoints']))
+        endpoints = service['endpoints'][0]
+
+        self.assertEqual('identity', service['type'])
+        self.assertEqual(4, len(endpoints))
+        self.assertEqual(self.PUBLIC_URL, endpoints['publicURL'])
+        self.assertEqual(self.ADMIN_URL, endpoints['adminURL'])
+        self.assertEqual(self.INTERNAL_URL, endpoints['internalURL'])
+        self.assertEqual(self.REGION_ONE, endpoints['region'])
+
+    def test_multi_region(self):
+        token = fixture.V3Token()
+        s = token.add_service(type='identity')
+
+        s.add_endpoint('internal', self.INTERNAL_URL, region=self.REGION_ONE)
+        s.add_endpoint('public', self.PUBLIC_URL, region=self.REGION_TWO)
+        s.add_endpoint('admin', self.ADMIN_URL, region=self.REGION_THREE)
+
+        auth_ref = access.AccessInfo.factory(body=token)
+        catalog_data = auth_ref.service_catalog.get_data()
+        catalog = auth_token._v3_to_v2_catalog(catalog_data)
+
+        self.assertEqual(1, len(catalog))
+        service = catalog[0]
+
+        # the 3 regions will come through as 3 separate endpoints
+        expected = [{'internalURL': self.INTERNAL_URL,
+                    'region': self.REGION_ONE},
+                    {'publicURL': self.PUBLIC_URL,
+                     'region': self.REGION_TWO},
+                    {'adminURL': self.ADMIN_URL,
+                     'region': self.REGION_THREE}]
+
+        self.assertEqual('identity', service['type'])
+        self.assertEqual(3, len(service['endpoints']))
+        for e in expected:
+            self.assertIn(e, expected)
+
+
+class DelayedAuthTests(BaseAuthTokenMiddlewareTest):
+
+    def test_header_in_401(self):
+        body = uuid.uuid4().hex
+        auth_uri = 'http://local.test'
+        conf = {'delay_auth_decision': 'True',
+                'auth_version': 'v3.0',
+                'auth_uri': auth_uri}
+
+        self.fake_app = new_app('401 Unauthorized', body)
+        self.set_middleware(conf=conf)
+
+        req = webob.Request.blank('/')
+        resp = self.middleware(req.environ, self.start_fake_response)
+
+        self.assertEqual([six.b(body)], resp)
+
+        self.assertEqual(401, self.response_status)
+        self.assertEqual("Keystone uri='%s'" % auth_uri,
+                         self.response_headers['WWW-Authenticate'])
+
+    def test_delayed_auth_values(self):
+        fake_app = new_app('401 Unauthorized', uuid.uuid4().hex)
+        middleware = auth_token.AuthProtocol(fake_app,
+                                             {'auth_uri': 'http://local.test'})
+        self.assertFalse(middleware._delay_auth_decision)
+
+        for v in ('True', '1', 'on', 'yes'):
+            conf = {'delay_auth_decision': v,
+                    'auth_uri': 'http://local.test'}
+
+            middleware = auth_token.AuthProtocol(fake_app, conf)
+            self.assertTrue(middleware._delay_auth_decision)
+
+        for v in ('False', '0', 'no'):
+            conf = {'delay_auth_decision': v,
+                    'auth_uri': 'http://local.test'}
+
+            middleware = auth_token.AuthProtocol(fake_app, conf)
+            self.assertFalse(middleware._delay_auth_decision)
+
+    def test_auth_plugin_with_no_tokens(self):
+        body = uuid.uuid4().hex
+        auth_uri = 'http://local.test'
+        conf = {'delay_auth_decision': True, 'auth_uri': auth_uri}
+        self.fake_app = new_app('200 OK', body)
+        self.set_middleware(conf=conf)
+
+        req = webob.Request.blank('/')
+        resp = self.middleware(req.environ, self.start_fake_response)
+
+        self.assertEqual([six.b(body)], resp)
+
+        token_auth = req.environ['keystone.token_auth']
+
+        self.assertFalse(token_auth.has_user_token)
+        self.assertIsNone(token_auth.user)
+        self.assertFalse(token_auth.has_service_token)
+        self.assertIsNone(token_auth.service)
+
+
+class CommonCompositeAuthTests(object):
+    """Test Composite authentication.
+
+    Test the behaviour of adding a service-token.
+    """
+
+    def test_composite_auth_ok(self):
+        req = webob.Request.blank('/')
+        token = self.token_dict['uuid_token_default']
+        service_token = self.token_dict['uuid_service_token_default']
+        req.headers['X-Auth-Token'] = token
+        req.headers['X-Service-Token'] = service_token
+        fake_logger = fixtures.FakeLogger(level=logging.DEBUG)
+        self.middleware.logger = self.useFixture(fake_logger)
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(200, self.response_status)
+        self.assertEqual([FakeApp.SUCCESS], body)
+        expected_env = dict(EXPECTED_V2_DEFAULT_ENV_RESPONSE)
+        expected_env.update(EXPECTED_V2_DEFAULT_SERVICE_ENV_RESPONSE)
+        self.assertIn('Received request from user: '
+                      'user_id %(HTTP_X_USER_ID)s, '
+                      'project_id %(HTTP_X_TENANT_ID)s, '
+                      'roles %(HTTP_X_ROLES)s '
+                      'service: user_id %(HTTP_X_SERVICE_USER_ID)s, '
+                      'project_id %(HTTP_X_SERVICE_PROJECT_ID)s, '
+                      'roles %(HTTP_X_SERVICE_ROLES)s' % expected_env,
+                      fake_logger.output)
+
+    def test_composite_auth_invalid_service_token(self):
+        req = webob.Request.blank('/')
+        token = self.token_dict['uuid_token_default']
+        service_token = 'invalid-service-token'
+        req.headers['X-Auth-Token'] = token
+        req.headers['X-Service-Token'] = service_token
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(401, self.response_status)
+        self.assertEqual([b'Authentication required'], body)
+
+    def test_composite_auth_no_service_token(self):
+        self.purge_service_token_expected_env()
+        req = webob.Request.blank('/')
+        token = self.token_dict['uuid_token_default']
+        req.headers['X-Auth-Token'] = token
+
+        # Ensure injection of service headers is not possible
+        for key, value in six.iteritems(self.service_token_expected_env):
+            header_key = key[len('HTTP_'):].replace('_', '-')
+            req.headers[header_key] = value
+        # Check arbitrary headers not removed
+        req.headers['X-Foo'] = 'Bar'
+        body = self.middleware(req.environ, self.start_fake_response)
+        for key in six.iterkeys(self.service_token_expected_env):
+            header_key = key[len('HTTP_'):].replace('_', '-')
+            self.assertFalse(req.headers.get(header_key))
+        self.assertEqual('Bar', req.headers.get('X-Foo'))
+        self.assertEqual(418, self.response_status)
+        self.assertEqual([FakeApp.FORBIDDEN], body)
+
+    def test_composite_auth_invalid_user_token(self):
+        req = webob.Request.blank('/')
+        token = 'invalid-token'
+        service_token = self.token_dict['uuid_service_token_default']
+        req.headers['X-Auth-Token'] = token
+        req.headers['X-Service-Token'] = service_token
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(401, self.response_status)
+        self.assertEqual([b'Authentication required'], body)
+
+    def test_composite_auth_no_user_token(self):
+        req = webob.Request.blank('/')
+        service_token = self.token_dict['uuid_service_token_default']
+        req.headers['X-Service-Token'] = service_token
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(401, self.response_status)
+        self.assertEqual([b'Authentication required'], body)
+
+    def test_composite_auth_delay_ok(self):
+        self.middleware._delay_auth_decision = True
+        req = webob.Request.blank('/')
+        token = self.token_dict['uuid_token_default']
+        service_token = self.token_dict['uuid_service_token_default']
+        req.headers['X-Auth-Token'] = token
+        req.headers['X-Service-Token'] = service_token
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(200, self.response_status)
+        self.assertEqual([FakeApp.SUCCESS], body)
+
+    def test_composite_auth_delay_invalid_service_token(self):
+        self.middleware._delay_auth_decision = True
+        self.purge_service_token_expected_env()
+        expected_env = {
+            'HTTP_X_SERVICE_IDENTITY_STATUS': 'Invalid',
+        }
+        self.update_expected_env(expected_env)
+
+        req = webob.Request.blank('/')
+        token = self.token_dict['uuid_token_default']
+        service_token = 'invalid-service-token'
+        req.headers['X-Auth-Token'] = token
+        req.headers['X-Service-Token'] = service_token
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(420, self.response_status)
+        self.assertEqual([FakeApp.FORBIDDEN], body)
+
+    def test_composite_auth_delay_invalid_service_and_user_tokens(self):
+        self.middleware._delay_auth_decision = True
+        self.purge_service_token_expected_env()
+        self.purge_token_expected_env()
+        expected_env = {
+            'HTTP_X_IDENTITY_STATUS': 'Invalid',
+            'HTTP_X_SERVICE_IDENTITY_STATUS': 'Invalid',
+        }
+        self.update_expected_env(expected_env)
+
+        req = webob.Request.blank('/')
+        token = 'invalid-user-token'
+        service_token = 'invalid-service-token'
+        req.headers['X-Auth-Token'] = token
+        req.headers['X-Service-Token'] = service_token
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(419, self.response_status)
+        self.assertEqual([FakeApp.FORBIDDEN], body)
+
+    def test_composite_auth_delay_no_service_token(self):
+        self.middleware._delay_auth_decision = True
+        self.purge_service_token_expected_env()
+
+        req = webob.Request.blank('/')
+        token = self.token_dict['uuid_token_default']
+        req.headers['X-Auth-Token'] = token
+
+        # Ensure injection of service headers is not possible
+        for key, value in six.iteritems(self.service_token_expected_env):
+            header_key = key[len('HTTP_'):].replace('_', '-')
+            req.headers[header_key] = value
+        # Check arbitrary headers not removed
+        req.headers['X-Foo'] = 'Bar'
+        body = self.middleware(req.environ, self.start_fake_response)
+        for key in six.iterkeys(self.service_token_expected_env):
+            header_key = key[len('HTTP_'):].replace('_', '-')
+            self.assertFalse(req.headers.get(header_key))
+        self.assertEqual('Bar', req.headers.get('X-Foo'))
+        self.assertEqual(418, self.response_status)
+        self.assertEqual([FakeApp.FORBIDDEN], body)
+
+    def test_composite_auth_delay_invalid_user_token(self):
+        self.middleware._delay_auth_decision = True
+        self.purge_token_expected_env()
+        expected_env = {
+            'HTTP_X_IDENTITY_STATUS': 'Invalid',
+        }
+        self.update_expected_env(expected_env)
+
+        req = webob.Request.blank('/')
+        token = 'invalid-token'
+        service_token = self.token_dict['uuid_service_token_default']
+        req.headers['X-Auth-Token'] = token
+        req.headers['X-Service-Token'] = service_token
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(403, self.response_status)
+        self.assertEqual([FakeApp.FORBIDDEN], body)
+
+    def test_composite_auth_delay_no_user_token(self):
+        self.middleware._delay_auth_decision = True
+        self.purge_token_expected_env()
+        expected_env = {
+            'HTTP_X_IDENTITY_STATUS': 'Invalid',
+        }
+        self.update_expected_env(expected_env)
+
+        req = webob.Request.blank('/')
+        service_token = self.token_dict['uuid_service_token_default']
+        req.headers['X-Service-Token'] = service_token
+        body = self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(403, self.response_status)
+        self.assertEqual([FakeApp.FORBIDDEN], body)
+
+
+class v2CompositeAuthTests(BaseAuthTokenMiddlewareTest,
+                           CommonCompositeAuthTests,
+                           testresources.ResourcedTestCase):
+    """Test auth_token middleware with v2 token based composite auth.
+
+    Execute the Composite auth class tests, but with the
+    auth_token middleware configured to expect v2 tokens back from
+    a keystone server.
+    """
+
+    resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+    def setUp(self):
+        super(v2CompositeAuthTests, self).setUp(
+            expected_env=EXPECTED_V2_DEFAULT_SERVICE_ENV_RESPONSE,
+            fake_app=CompositeFakeApp)
+
+        uuid_token_default = self.examples.UUID_TOKEN_DEFAULT
+        uuid_service_token_default = self.examples.UUID_SERVICE_TOKEN_DEFAULT
+        self.token_dict = {
+            'uuid_token_default': uuid_token_default,
+            'uuid_service_token_default': uuid_service_token_default,
+        }
+
+        self.requests.get(BASE_URI,
+                          json=VERSION_LIST_v2,
+                          status_code=300)
+
+        self.requests.post('%s/v2.0/tokens' % BASE_URI,
+                           text=FAKE_ADMIN_TOKEN)
+
+        self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI,
+                          text=self.examples.SIGNED_REVOCATION_LIST,
+                          status_code=200)
+
+        for token in (self.examples.UUID_TOKEN_DEFAULT,
+                      self.examples.UUID_SERVICE_TOKEN_DEFAULT,):
+            self.requests.get('%s/v2.0/tokens/%s' % (BASE_URI, token),
+                              text=self.examples.JSON_TOKEN_RESPONSES[token])
+
+        for invalid_uri in ("%s/v2.0/tokens/invalid-token" % BASE_URI,
+                            "%s/v2.0/tokens/invalid-service-token" % BASE_URI):
+            self.requests.get(invalid_uri, text='', status_code=404)
+
+        self.token_expected_env = dict(EXPECTED_V2_DEFAULT_ENV_RESPONSE)
+        self.service_token_expected_env = dict(
+            EXPECTED_V2_DEFAULT_SERVICE_ENV_RESPONSE)
+        self.set_middleware()
+
+
+class v3CompositeAuthTests(BaseAuthTokenMiddlewareTest,
+                           CommonCompositeAuthTests,
+                           testresources.ResourcedTestCase):
+    """Test auth_token middleware with v3 token based composite auth.
+
+    Execute the Composite auth class tests, but with the
+    auth_token middleware configured to expect v3 tokens back from
+    a keystone server.
+    """
+
+    resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+    def setUp(self):
+        super(v3CompositeAuthTests, self).setUp(
+            auth_version='v3.0',
+            fake_app=v3CompositeFakeApp)
+
+        uuid_token_default = self.examples.v3_UUID_TOKEN_DEFAULT
+        uuid_serv_token_default = self.examples.v3_UUID_SERVICE_TOKEN_DEFAULT
+        self.token_dict = {
+            'uuid_token_default': uuid_token_default,
+            'uuid_service_token_default': uuid_serv_token_default,
+        }
+
+        self.requests.get(BASE_URI, json=VERSION_LIST_v3, status_code=300)
+
+        # TODO(jamielennox): auth_token middleware uses a v2 admin token
+        # regardless of the auth_version that is set.
+        self.requests.post('%s/v2.0/tokens' % BASE_URI,
+                           text=FAKE_ADMIN_TOKEN)
+
+        # TODO(jamielennox): there is no v3 revocation url yet, it uses v2
+        self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI,
+                          text=self.examples.SIGNED_REVOCATION_LIST)
+
+        self.requests.get('%s/v3/auth/tokens' % BASE_URI,
+                          text=self.token_response)
+
+        self.token_expected_env = dict(EXPECTED_V2_DEFAULT_ENV_RESPONSE)
+        self.token_expected_env.update(EXPECTED_V3_DEFAULT_ENV_ADDITIONS)
+        self.service_token_expected_env = dict(
+            EXPECTED_V2_DEFAULT_SERVICE_ENV_RESPONSE)
+        self.service_token_expected_env.update(
+            EXPECTED_V3_DEFAULT_SERVICE_ENV_ADDITIONS)
+        self.set_middleware()
+
+    def token_response(self, request, context):
+        auth_id = request.headers.get('X-Auth-Token')
+        token_id = request.headers.get('X-Subject-Token')
+        self.assertEqual(auth_id, FAKE_ADMIN_TOKEN_ID)
+
+        status = 200
+        response = ""
+
+        if token_id == ERROR_TOKEN:
+            raise exceptions.ConnectionError("Network connection error.")
+
+        try:
+            response = self.examples.JSON_TOKEN_RESPONSES[token_id]
+        except KeyError:
+            status = 404
+
+        context.status_code = status
+        return response
+
+
+class OtherTests(BaseAuthTokenMiddlewareTest):
+
+    def setUp(self):
+        super(OtherTests, self).setUp()
+        self.logger = self.useFixture(fixtures.FakeLogger())
+        self.cfg = self.useFixture(cfg_fixture.Config())
+
+    def test_unknown_server_versions(self):
+        versions = fixture.DiscoveryList(v2=False, v3_id='v4', href=BASE_URI)
+        self.set_middleware()
+
+        self.requests.get(BASE_URI, json=versions, status_code=300)
+
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = uuid.uuid4().hex
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(503, self.response_status)
+
+        self.assertIn('versions [v3.0, v2.0]', self.logger.output)
+
+    def _assert_auth_version(self, conf_version, identity_server_version):
+        self.set_middleware(conf={'auth_version': conf_version})
+        identity_server = self.middleware._create_identity_server()
+        self.assertEqual(identity_server_version,
+                         identity_server.auth_version)
+
+    def test_micro_version(self):
+        self._assert_auth_version('v2', (2, 0))
+        self._assert_auth_version('v2.0', (2, 0))
+        self._assert_auth_version('v3', (3, 0))
+        self._assert_auth_version('v3.0', (3, 0))
+        self._assert_auth_version('v3.1', (3, 0))
+        self._assert_auth_version('v3.2', (3, 0))
+        self._assert_auth_version('v3.9', (3, 0))
+        self._assert_auth_version('v3.3.1', (3, 0))
+        self._assert_auth_version('v3.3.5', (3, 0))
+
+    def test_default_auth_version(self):
+        # VERSION_LIST_v3 contains both v2 and v3 version elements
+        self.requests.get(BASE_URI, json=VERSION_LIST_v3, status_code=300)
+        self._assert_auth_version(None, (3, 0))
+
+        # VERSION_LIST_v2 contains only v2 version elements
+        self.requests.get(BASE_URI, json=VERSION_LIST_v2, status_code=300)
+        self._assert_auth_version(None, (2, 0))
+
+    def test_unsupported_auth_version(self):
+        # If the requested version isn't supported we will use v2
+        self._assert_auth_version('v1', (2, 0))
+        self._assert_auth_version('v10', (2, 0))
+
+
+class AuthProtocolLoadingTests(BaseAuthTokenMiddlewareTest):
+
+    AUTH_URL = 'http://auth.url/prefix'
+    DISC_URL = 'http://disc.url/prefix'
+    KEYSTONE_BASE_URL = 'http://keystone.url/prefix'
+    CRUD_URL = 'http://crud.url/prefix'
+
+    # NOTE(jamielennox): use the /v2.0 prefix here because this is what's most
+    # likely to be in the service catalog and we should be able to ignore it.
+    KEYSTONE_URL = KEYSTONE_BASE_URL + '/v2.0'
+
+    def setUp(self):
+        super(AuthProtocolLoadingTests, self).setUp()
+        self.cfg = self.useFixture(cfg_fixture.Config())
+
+        self.project_id = uuid.uuid4().hex
+
+        # first touch is to discover the available versions at the auth_url
+        self.requests.get(self.AUTH_URL,
+                          json=fixture.DiscoveryList(href=self.DISC_URL),
+                          status_code=300)
+
+        # then we do discovery on the URL from the service catalog. In practice
+        # this is mostly the same URL as before but test the full range.
+        self.requests.get(self.KEYSTONE_BASE_URL + '/',
+                          json=fixture.DiscoveryList(href=self.CRUD_URL),
+                          status_code=300)
+
+    def good_request(self, app):
+        # admin_token is the token that the service will get back from auth
+        admin_token_id = uuid.uuid4().hex
+        admin_token = fixture.V3Token(project_id=self.project_id)
+        s = admin_token.add_service('identity', name='keystone')
+        s.add_standard_endpoints(admin=self.KEYSTONE_URL)
+
+        self.requests.post(self.DISC_URL + '/v3/auth/tokens',
+                           json=admin_token,
+                           headers={'X-Subject-Token': admin_token_id})
+
+        # user_token is the data from the user's inputted token
+        user_token_id = uuid.uuid4().hex
+        user_token = fixture.V3Token()
+        user_token.set_project_scope()
+
+        request_headers = {'X-Subject-Token': user_token_id,
+                           'X-Auth-Token': admin_token_id}
+
+        self.requests.get(self.CRUD_URL + '/v3/auth/tokens',
+                          request_headers=request_headers,
+                          json=user_token)
+
+        req = webob.Request.blank('/')
+        req.headers['X-Auth-Token'] = user_token_id
+        resp = app(req.environ, self.start_fake_response)
+
+        self.assertEqual(200, self.response_status)
+        return resp
+
+    def test_loading_password_plugin(self):
+        # the password options aren't set on config until loading time, but we
+        # need them set so we can override the values for testing, so force it
+        opts = auth.get_plugin_options('password')
+        self.cfg.register_opts(opts, group=_base.AUTHTOKEN_GROUP)
+
+        project_id = uuid.uuid4().hex
+
+        # configure the authentication options
+        self.cfg.config(auth_plugin='password',
+                        username='testuser',
+                        password='testpass',
+                        auth_url=self.AUTH_URL,
+                        project_id=project_id,
+                        user_domain_id='userdomainid',
+                        group=_base.AUTHTOKEN_GROUP)
+
+        body = uuid.uuid4().hex
+        app = auth_token.AuthProtocol(new_app('200 OK', body)(), {})
+
+        resp = self.good_request(app)
+        self.assertEqual(six.b(body), resp[0])
+
+    @staticmethod
+    def get_plugin(app):
+        return app._identity_server._adapter.auth
+
+    def test_invalid_plugin_fails_to_intialize(self):
+        self.cfg.config(auth_plugin=uuid.uuid4().hex,
+                        group=_base.AUTHTOKEN_GROUP)
+
+        self.assertRaises(
+            exceptions.NoMatchingPlugin,
+            lambda: auth_token.AuthProtocol(new_app('200 OK', '')(), {}))
+
+    def test_plugin_loading_mixed_opts(self):
+        # some options via override and some via conf
+        opts = auth.get_plugin_options('password')
+        self.cfg.register_opts(opts, group=_base.AUTHTOKEN_GROUP)
+
+        username = 'testuser'
+        password = 'testpass'
+
+        # configure the authentication options
+        self.cfg.config(auth_plugin='password',
+                        password=password,
+                        project_id=self.project_id,
+                        user_domain_id='userdomainid',
+                        group=_base.AUTHTOKEN_GROUP)
+
+        conf = {'username': username, 'auth_url': self.AUTH_URL}
+
+        body = uuid.uuid4().hex
+        app = auth_token.AuthProtocol(new_app('200 OK', body)(), conf)
+
+        resp = self.good_request(app)
+        self.assertEqual(six.b(body), resp[0])
+
+        plugin = self.get_plugin(app)
+
+        self.assertEqual(self.AUTH_URL, plugin.auth_url)
+        self.assertEqual(username, plugin._username)
+        self.assertEqual(password, plugin._password)
+        self.assertEqual(self.project_id, plugin._project_id)
+
+    def test_plugin_loading_with_auth_section(self):
+        # some options via override and some via conf
+        section = 'testsection'
+        username = 'testuser'
+        password = 'testpass'
+
+        auth.register_conf_options(self.cfg.conf, group=section)
+        opts = auth.get_plugin_options('password')
+        self.cfg.register_opts(opts, group=section)
+
+        # configure the authentication options
+        self.cfg.config(auth_section=section, group=_base.AUTHTOKEN_GROUP)
+        self.cfg.config(auth_plugin='password',
+                        password=password,
+                        project_id=self.project_id,
+                        user_domain_id='userdomainid',
+                        group=section)
+
+        conf = {'username': username, 'auth_url': self.AUTH_URL}
+
+        body = uuid.uuid4().hex
+        app = auth_token.AuthProtocol(new_app('200 OK', body)(), conf)
+
+        resp = self.good_request(app)
+        self.assertEqual(six.b(body), resp[0])
+
+        plugin = self.get_plugin(app)
+
+        self.assertEqual(self.AUTH_URL, plugin.auth_url)
+        self.assertEqual(username, plugin._username)
+        self.assertEqual(password, plugin._password)
+        self.assertEqual(self.project_id, plugin._project_id)
+
+
+def load_tests(loader, tests, pattern):
+    return testresources.OptimisingTestSuite(tests)
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_connection_pool.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_connection_pool.py
new file mode 100644 (file)
index 0000000..074d1e5
--- /dev/null
@@ -0,0 +1,118 @@
+# 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.
+
+import time
+
+import mock
+from six.moves import queue
+import testtools
+from testtools import matchers
+
+from keystonemiddleware.auth_token import _memcache_pool
+from keystonemiddleware.tests.unit import utils
+
+
+class _TestConnectionPool(_memcache_pool.ConnectionPool):
+    destroyed_value = 'destroyed'
+
+    def _create_connection(self):
+        return mock.MagicMock()
+
+    def _destroy_connection(self, conn):
+        conn(self.destroyed_value)
+
+
+class TestConnectionPool(utils.TestCase):
+    def setUp(self):
+        super(TestConnectionPool, self).setUp()
+        self.unused_timeout = 10
+        self.maxsize = 2
+        self.connection_pool = _TestConnectionPool(
+            maxsize=self.maxsize,
+            unused_timeout=self.unused_timeout)
+
+    def test_get_context_manager(self):
+        self.assertThat(self.connection_pool.queue, matchers.HasLength(0))
+        with self.connection_pool.acquire() as conn:
+            self.assertEqual(1, self.connection_pool._acquired)
+        self.assertEqual(0, self.connection_pool._acquired)
+        self.assertThat(self.connection_pool.queue, matchers.HasLength(1))
+        self.assertEqual(conn, self.connection_pool.queue[0].connection)
+
+    def test_cleanup_pool(self):
+        self.test_get_context_manager()
+        newtime = time.time() + self.unused_timeout * 2
+        non_expired_connection = _memcache_pool._PoolItem(
+            ttl=(newtime * 2),
+            connection=mock.MagicMock())
+        self.connection_pool.queue.append(non_expired_connection)
+        self.assertThat(self.connection_pool.queue, matchers.HasLength(2))
+        with mock.patch.object(time, 'time', return_value=newtime):
+            conn = self.connection_pool.queue[0].connection
+            with self.connection_pool.acquire():
+                pass
+            conn.assert_has_calls(
+                [mock.call(self.connection_pool.destroyed_value)])
+        self.assertThat(self.connection_pool.queue, matchers.HasLength(1))
+        self.assertEqual(0, non_expired_connection.connection.call_count)
+
+    def test_acquire_conn_exception_returns_acquired_count(self):
+        class TestException(Exception):
+            pass
+
+        with mock.patch.object(_TestConnectionPool, '_create_connection',
+                               side_effect=TestException):
+            with testtools.ExpectedException(TestException):
+                with self.connection_pool.acquire():
+                    pass
+            self.assertThat(self.connection_pool.queue,
+                            matchers.HasLength(0))
+            self.assertEqual(0, self.connection_pool._acquired)
+
+    def test_connection_pool_limits_maximum_connections(self):
+        # NOTE(morganfainberg): To ensure we don't lockup tests until the
+        # job limit, explicitly call .get_nowait() and .put_nowait() in this
+        # case.
+        conn1 = self.connection_pool.get_nowait()
+        conn2 = self.connection_pool.get_nowait()
+
+        # Use a nowait version to raise an Empty exception indicating we would
+        # not get another connection until one is placed back into the queue.
+        self.assertRaises(queue.Empty, self.connection_pool.get_nowait)
+
+        # Place the connections back into the pool.
+        self.connection_pool.put_nowait(conn1)
+        self.connection_pool.put_nowait(conn2)
+
+        # Make sure we can get a connection out of the pool again.
+        self.connection_pool.get_nowait()
+
+    def test_connection_pool_maximum_connection_get_timeout(self):
+        connection_pool = _TestConnectionPool(
+            maxsize=1,
+            unused_timeout=self.unused_timeout,
+            conn_get_timeout=0)
+
+        def _acquire_connection():
+            with connection_pool.acquire():
+                pass
+
+        # Make sure we've consumed the only available connection from the pool
+        conn = connection_pool.get_nowait()
+
+        self.assertRaises(_memcache_pool.ConnectionGetTimeoutException,
+                          _acquire_connection)
+
+        # Put the connection back and ensure we can acquire the connection
+        # after it is available.
+        connection_pool.put_nowait(conn)
+        _acquire_connection()
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_memcache_crypt.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_memcache_crypt.py
new file mode 100644 (file)
index 0000000..75c7f75
--- /dev/null
@@ -0,0 +1,97 @@
+#    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.
+
+import six
+import testtools
+
+from keystonemiddleware.auth_token import _memcache_crypt as memcache_crypt
+
+
+class MemcacheCryptPositiveTests(testtools.TestCase):
+    def _setup_keys(self, strategy):
+        return memcache_crypt.derive_keys(b'token', b'secret', strategy)
+
+    def test_constant_time_compare(self):
+        # make sure it works as a compare, the "constant time" aspect
+        # isn't appropriate to test in unittests
+        ctc = memcache_crypt.constant_time_compare
+        self.assertTrue(ctc('abcd', 'abcd'))
+        self.assertTrue(ctc('', ''))
+        self.assertFalse(ctc('abcd', 'efgh'))
+        self.assertFalse(ctc('abc', 'abcd'))
+        self.assertFalse(ctc('abc', 'abc\x00'))
+        self.assertFalse(ctc('', 'abc'))
+
+        # For Python 3, we want to test these functions with both str and bytes
+        # as input.
+        if six.PY3:
+            self.assertTrue(ctc(b'abcd', b'abcd'))
+            self.assertTrue(ctc(b'', b''))
+            self.assertFalse(ctc(b'abcd', b'efgh'))
+            self.assertFalse(ctc(b'abc', b'abcd'))
+            self.assertFalse(ctc(b'abc', b'abc\x00'))
+            self.assertFalse(ctc(b'', b'abc'))
+
+    def test_derive_keys(self):
+        keys = self._setup_keys(b'strategy')
+        self.assertEqual(len(keys['ENCRYPTION']),
+                         len(keys['CACHE_KEY']))
+        self.assertEqual(len(keys['CACHE_KEY']),
+                         len(keys['MAC']))
+        self.assertNotEqual(keys['ENCRYPTION'],
+                            keys['MAC'])
+        self.assertIn('strategy', keys.keys())
+
+    def test_key_strategy_diff(self):
+        k1 = self._setup_keys(b'MAC')
+        k2 = self._setup_keys(b'ENCRYPT')
+        self.assertNotEqual(k1, k2)
+
+    def test_sign_data(self):
+        keys = self._setup_keys(b'MAC')
+        sig = memcache_crypt.sign_data(keys['MAC'], b'data')
+        self.assertEqual(len(sig), memcache_crypt.DIGEST_LENGTH_B64)
+
+    def test_encryption(self):
+        keys = self._setup_keys(b'ENCRYPT')
+        # what you put in is what you get out
+        for data in [b'data', b'1234567890123456', b'\x00\xFF' * 13
+                     ] + [six.int2byte(x % 256) * x for x in range(768)]:
+            crypt = memcache_crypt.encrypt_data(keys['ENCRYPTION'], data)
+            decrypt = memcache_crypt.decrypt_data(keys['ENCRYPTION'], crypt)
+            self.assertEqual(data, decrypt)
+            self.assertRaises(memcache_crypt.DecryptError,
+                              memcache_crypt.decrypt_data,
+                              keys['ENCRYPTION'], crypt[:-1])
+
+    def test_protect_wrappers(self):
+        data = b'My Pretty Little Data'
+        for strategy in [b'MAC', b'ENCRYPT']:
+            keys = self._setup_keys(strategy)
+            protected = memcache_crypt.protect_data(keys, data)
+            self.assertNotEqual(protected, data)
+            if strategy == b'ENCRYPT':
+                self.assertNotIn(data, protected)
+            unprotected = memcache_crypt.unprotect_data(keys, protected)
+            self.assertEqual(data, unprotected)
+            self.assertRaises(memcache_crypt.InvalidMacError,
+                              memcache_crypt.unprotect_data,
+                              keys, protected[:-1])
+            self.assertIsNone(memcache_crypt.unprotect_data(keys, None))
+
+    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/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_revocations.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_revocations.py
new file mode 100644 (file)
index 0000000..d144bb6
--- /dev/null
@@ -0,0 +1,65 @@
+# Copyright 2014 IBM Corp.
+#
+#   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.
+
+import datetime
+import json
+import shutil
+import uuid
+
+import mock
+import testtools
+
+from keystonemiddleware.auth_token import _exceptions as exc
+from keystonemiddleware.auth_token import _revocations
+from keystonemiddleware.auth_token import _signing_dir
+
+
+class RevocationsTests(testtools.TestCase):
+
+    def _check_with_list(self, revoked_list, token_ids):
+        directory_name = '/tmp/%s' % uuid.uuid4().hex
+        signing_directory = _signing_dir.SigningDirectory(directory_name)
+        self.addCleanup(shutil.rmtree, directory_name)
+
+        identity_server = mock.Mock()
+
+        verify_result_obj = {
+            'revoked': list({'id': r} for r in revoked_list)
+        }
+        cms_verify = mock.Mock(return_value=json.dumps(verify_result_obj))
+
+        revocations = _revocations.Revocations(
+            timeout=datetime.timedelta(1), signing_directory=signing_directory,
+            identity_server=identity_server, cms_verify=cms_verify)
+
+        revocations.check(token_ids)
+
+    def test_check_empty_list(self):
+        # When the identity server returns an empty list, a token isn't
+        # revoked.
+
+        revoked_tokens = []
+        token_ids = [uuid.uuid4().hex]
+        # No assert because this would raise
+        self._check_with_list(revoked_tokens, token_ids)
+
+    def test_check_revoked(self):
+        # When the identity server returns a list with a token in it, that
+        # token is revoked.
+
+        token_id = uuid.uuid4().hex
+        revoked_tokens = [token_id]
+        token_ids = [token_id]
+        self.assertRaises(exc.InvalidToken,
+                          self._check_with_list, revoked_tokens, token_ids)
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_signing_dir.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_signing_dir.py
new file mode 100644 (file)
index 0000000..bef6274
--- /dev/null
@@ -0,0 +1,138 @@
+# 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.
+
+import os
+import shutil
+import stat
+import uuid
+
+import testtools
+
+from keystonemiddleware.auth_token import _signing_dir
+
+
+class SigningDirectoryTests(testtools.TestCase):
+
+    def test_directory_created_when_doesnt_exist(self):
+        # When _SigningDirectory is created, if the directory doesn't exist
+        # it's created with the expected permissions.
+        tmp_name = uuid.uuid4().hex
+        parent_directory = '/tmp/%s' % tmp_name
+        directory_name = '/tmp/%s/%s' % ((tmp_name,) * 2)
+
+        # Directories are created by __init__.
+        _signing_dir.SigningDirectory(directory_name)
+        self.addCleanup(shutil.rmtree, parent_directory)
+
+        self.assertTrue(os.path.isdir(directory_name))
+        self.assertTrue(os.access(directory_name, os.W_OK))
+        self.assertEqual(os.stat(directory_name).st_uid, os.getuid())
+        self.assertEqual(stat.S_IMODE(os.stat(directory_name).st_mode),
+                         stat.S_IRWXU)
+
+    def test_use_directory_already_exists(self):
+        # The directory can already exist.
+
+        tmp_name = uuid.uuid4().hex
+        parent_directory = '/tmp/%s' % tmp_name
+        directory_name = '/tmp/%s/%s' % ((tmp_name,) * 2)
+        os.makedirs(directory_name, stat.S_IRWXU)
+        self.addCleanup(shutil.rmtree, parent_directory)
+
+        _signing_dir.SigningDirectory(directory_name)
+
+    def test_write_file(self):
+        # write_file when the file doesn't exist creates the file.
+
+        signing_directory = _signing_dir.SigningDirectory()
+        self.addCleanup(shutil.rmtree, signing_directory._directory_name)
+
+        file_name = self.getUniqueString()
+        contents = self.getUniqueString()
+        signing_directory.write_file(file_name, contents)
+
+        file_path = signing_directory.calc_path(file_name)
+        with open(file_path) as f:
+            actual_contents = f.read()
+
+        self.assertEqual(contents, actual_contents)
+
+    def test_replace_file(self):
+        # write_file when the file already exists overwrites it.
+
+        signing_directory = _signing_dir.SigningDirectory()
+        self.addCleanup(shutil.rmtree, signing_directory._directory_name)
+
+        file_name = self.getUniqueString()
+        orig_contents = self.getUniqueString()
+        signing_directory.write_file(file_name, orig_contents)
+
+        new_contents = self.getUniqueString()
+        signing_directory.write_file(file_name, new_contents)
+
+        file_path = signing_directory.calc_path(file_name)
+        with open(file_path) as f:
+            actual_contents = f.read()
+
+        self.assertEqual(new_contents, actual_contents)
+
+    def test_recreate_directory(self):
+        # If the original directory is lost, it gets recreated when a file
+        # is written.
+
+        signing_directory = _signing_dir.SigningDirectory()
+        self.addCleanup(shutil.rmtree, signing_directory._directory_name)
+
+        # Delete the directory.
+        shutil.rmtree(signing_directory._directory_name)
+
+        file_name = self.getUniqueString()
+        contents = self.getUniqueString()
+        signing_directory.write_file(file_name, contents)
+
+        actual_contents = signing_directory.read_file(file_name)
+        self.assertEqual(contents, actual_contents)
+
+    def test_read_file(self):
+        # Can read a file that was written.
+
+        signing_directory = _signing_dir.SigningDirectory()
+        self.addCleanup(shutil.rmtree, signing_directory._directory_name)
+
+        file_name = self.getUniqueString()
+        contents = self.getUniqueString()
+        signing_directory.write_file(file_name, contents)
+
+        actual_contents = signing_directory.read_file(file_name)
+
+        self.assertEqual(contents, actual_contents)
+
+    def test_read_file_doesnt_exist(self):
+        # Show what happens when try to read a file that wasn't written.
+
+        signing_directory = _signing_dir.SigningDirectory()
+        self.addCleanup(shutil.rmtree, signing_directory._directory_name)
+
+        file_name = self.getUniqueString()
+        self.assertRaises(IOError, signing_directory.read_file, file_name)
+
+    def test_calc_path(self):
+        # calc_path returns the actual filename built from the directory name.
+
+        signing_directory = _signing_dir.SigningDirectory()
+        self.addCleanup(shutil.rmtree, signing_directory._directory_name)
+
+        file_name = self.getUniqueString()
+        actual_path = signing_directory.calc_path(file_name)
+        expected_path = os.path.join(signing_directory._directory_name,
+                                     file_name)
+        self.assertEqual(expected_path, actual_path)
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_utils.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/auth_token/test_utils.py
new file mode 100644 (file)
index 0000000..fcd1e62
--- /dev/null
@@ -0,0 +1,37 @@
+# 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.
+
+import testtools
+
+from keystonemiddleware.auth_token import _utils
+
+
+class TokenEncodingTest(testtools.TestCase):
+
+    def test_unquoted_token(self):
+        self.assertEqual('foo%20bar', _utils.safe_quote('foo bar'))
+
+    def test_quoted_token(self):
+        self.assertEqual('foo%20bar', _utils.safe_quote('foo%20bar'))
+
+    def test_messages_encoded_as_bytes(self):
+        """Test that string are passed around as bytes for PY3."""
+        msg = "This is an error"
+
+        class FakeResp(_utils.MiniResp):
+            def __init__(self, error, env):
+                super(FakeResp, self).__init__(error, env)
+
+        fake_resp = FakeResp(msg, dict(REQUEST_METHOD='GET'))
+        # On Py2 .encode() don't do much but that's better than to
+        # have a ifdef with six.PY3
+        self.assertEqual(msg.encode(), fake_resp.body[0])
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/client_fixtures.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/client_fixtures.py
new file mode 100644 (file)
index 0000000..ee4111e
--- /dev/null
@@ -0,0 +1,452 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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.
+
+import os
+
+import fixtures
+from keystoneclient.common import cms
+from keystoneclient import fixture
+from keystoneclient import utils
+from oslo_serialization import jsonutils
+from oslo_utils import timeutils
+import six
+import testresources
+
+
+TESTDIR = os.path.dirname(os.path.abspath(__file__))
+ROOTDIR = os.path.normpath(os.path.join(TESTDIR, '..', '..', '..'))
+CERTDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'certs')
+CMSDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'cms')
+KEYDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'private')
+
+
+def _hash_signed_token_safe(signed_text, **kwargs):
+    if isinstance(signed_text, six.text_type):
+        signed_text = signed_text.encode('utf-8')
+    return utils.hash_signed_token(signed_text, **kwargs)
+
+
+class Examples(fixtures.Fixture):
+    """Example tokens and certs loaded from the examples directory.
+
+    To use this class correctly, the module needs to override the test suite
+    class to use testresources.OptimisingTestSuite (otherwise the files will
+    be read on every test). This is done by defining a load_tests function
+    in the module, like this:
+
+    def load_tests(loader, tests, pattern):
+        return testresources.OptimisingTestSuite(tests)
+
+    (see http://docs.python.org/2/library/unittest.html#load-tests-protocol )
+
+    """
+
+    def setUp(self):
+        super(Examples, self).setUp()
+
+        # The data for several tests are signed using openssl and are stored in
+        # files in the signing subdirectory.  In order to keep the values
+        # consistent between the tests and the signed documents, we read them
+        # in for use in the tests.
+        with open(os.path.join(CMSDIR, 'auth_token_scoped.json')) as f:
+            self.TOKEN_SCOPED_DATA = cms.cms_to_token(f.read())
+
+        with open(os.path.join(CMSDIR, 'auth_token_scoped.pem')) as f:
+            self.SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read())
+        self.SIGNED_TOKEN_SCOPED_HASH = _hash_signed_token_safe(
+            self.SIGNED_TOKEN_SCOPED)
+        self.SIGNED_TOKEN_SCOPED_HASH_SHA256 = _hash_signed_token_safe(
+            self.SIGNED_TOKEN_SCOPED, mode='sha256')
+        with open(os.path.join(CMSDIR, 'auth_token_unscoped.pem')) as f:
+            self.SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read())
+        with open(os.path.join(CMSDIR, 'auth_v3_token_scoped.pem')) as f:
+            self.SIGNED_v3_TOKEN_SCOPED = cms.cms_to_token(f.read())
+        self.SIGNED_v3_TOKEN_SCOPED_HASH = _hash_signed_token_safe(
+            self.SIGNED_v3_TOKEN_SCOPED)
+        self.SIGNED_v3_TOKEN_SCOPED_HASH_SHA256 = _hash_signed_token_safe(
+            self.SIGNED_v3_TOKEN_SCOPED, mode='sha256')
+        with open(os.path.join(CMSDIR, 'auth_token_revoked.pem')) as f:
+            self.REVOKED_TOKEN = cms.cms_to_token(f.read())
+        with open(os.path.join(CMSDIR, 'auth_token_scoped_expired.pem')) as f:
+            self.SIGNED_TOKEN_SCOPED_EXPIRED = cms.cms_to_token(f.read())
+        with open(os.path.join(CMSDIR, 'auth_v3_token_revoked.pem')) as f:
+            self.REVOKED_v3_TOKEN = cms.cms_to_token(f.read())
+        with open(os.path.join(CMSDIR, 'auth_token_scoped.pkiz')) as f:
+            self.SIGNED_TOKEN_SCOPED_PKIZ = cms.cms_to_token(f.read())
+        with open(os.path.join(CMSDIR, 'auth_token_unscoped.pkiz')) as f:
+            self.SIGNED_TOKEN_UNSCOPED_PKIZ = cms.cms_to_token(f.read())
+        with open(os.path.join(CMSDIR, 'auth_v3_token_scoped.pkiz')) as f:
+            self.SIGNED_v3_TOKEN_SCOPED_PKIZ = cms.cms_to_token(f.read())
+        with open(os.path.join(CMSDIR, 'auth_token_revoked.pkiz')) as f:
+            self.REVOKED_TOKEN_PKIZ = cms.cms_to_token(f.read())
+        with open(os.path.join(CMSDIR,
+                               'auth_token_scoped_expired.pkiz')) as f:
+            self.SIGNED_TOKEN_SCOPED_EXPIRED_PKIZ = cms.cms_to_token(f.read())
+        with open(os.path.join(CMSDIR, 'auth_v3_token_revoked.pkiz')) as f:
+            self.REVOKED_v3_TOKEN_PKIZ = cms.cms_to_token(f.read())
+        with open(os.path.join(CMSDIR, 'revocation_list.json')) as f:
+            self.REVOCATION_LIST = jsonutils.loads(f.read())
+        with open(os.path.join(CMSDIR, 'revocation_list.pem')) as f:
+            self.SIGNED_REVOCATION_LIST = jsonutils.dumps({'signed': f.read()})
+
+        self.SIGNING_CERT_FILE = os.path.join(CERTDIR, 'signing_cert.pem')
+        with open(self.SIGNING_CERT_FILE) as f:
+            self.SIGNING_CERT = f.read()
+
+        self.KERBEROS_BIND = 'USER@REALM'
+
+        self.SIGNING_KEY_FILE = os.path.join(KEYDIR, 'signing_key.pem')
+        with open(self.SIGNING_KEY_FILE) as f:
+            self.SIGNING_KEY = f.read()
+
+        self.SIGNING_CA_FILE = os.path.join(CERTDIR, 'cacert.pem')
+        with open(self.SIGNING_CA_FILE) as f:
+            self.SIGNING_CA = f.read()
+
+        self.UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d"
+        self.UUID_TOKEN_NO_SERVICE_CATALOG = '8286720fbe4941e69fa8241723bb02df'
+        self.UUID_TOKEN_UNSCOPED = '731f903721c14827be7b2dc912af7776'
+        self.UUID_TOKEN_BIND = '3fc54048ad64405c98225ce0897af7c5'
+        self.UUID_TOKEN_UNKNOWN_BIND = '8885fdf4d42e4fb9879e6379fa1eaf48'
+        self.VALID_DIABLO_TOKEN = 'b0cf19b55dbb4f20a6ee18e6c6cf1726'
+        self.v3_UUID_TOKEN_DEFAULT = '5603457654b346fdbb93437bfe76f2f1'
+        self.v3_UUID_TOKEN_UNSCOPED = 'd34835fdaec447e695a0a024d84f8d79'
+        self.v3_UUID_TOKEN_DOMAIN_SCOPED = 'e8a7b63aaa4449f38f0c5c05c3581792'
+        self.v3_UUID_TOKEN_BIND = '2f61f73e1c854cbb9534c487f9bd63c2'
+        self.v3_UUID_TOKEN_UNKNOWN_BIND = '7ed9781b62cd4880b8d8c6788ab1d1e2'
+
+        self.UUID_SERVICE_TOKEN_DEFAULT = 'fe4c0710ec2f492748596c1b53ab124'
+        self.v3_UUID_SERVICE_TOKEN_DEFAULT = 'g431071bbc2f492748596c1b53cb229'
+
+        revoked_token = self.REVOKED_TOKEN
+        if isinstance(revoked_token, six.text_type):
+            revoked_token = revoked_token.encode('utf-8')
+        self.REVOKED_TOKEN_HASH = utils.hash_signed_token(revoked_token)
+        self.REVOKED_TOKEN_HASH_SHA256 = utils.hash_signed_token(revoked_token,
+                                                                 mode='sha256')
+        self.REVOKED_TOKEN_LIST = (
+            {'revoked': [{'id': self.REVOKED_TOKEN_HASH,
+                          'expires': timeutils.utcnow()}]})
+        self.REVOKED_TOKEN_LIST_JSON = jsonutils.dumps(self.REVOKED_TOKEN_LIST)
+
+        revoked_v3_token = self.REVOKED_v3_TOKEN
+        if isinstance(revoked_v3_token, six.text_type):
+            revoked_v3_token = revoked_v3_token.encode('utf-8')
+        self.REVOKED_v3_TOKEN_HASH = utils.hash_signed_token(revoked_v3_token)
+        hash = utils.hash_signed_token(revoked_v3_token, mode='sha256')
+        self.REVOKED_v3_TOKEN_HASH_SHA256 = hash
+        self.REVOKED_v3_TOKEN_LIST = (
+            {'revoked': [{'id': self.REVOKED_v3_TOKEN_HASH,
+                          'expires': timeutils.utcnow()}]})
+        self.REVOKED_v3_TOKEN_LIST_JSON = jsonutils.dumps(
+            self.REVOKED_v3_TOKEN_LIST)
+
+        revoked_token_pkiz = self.REVOKED_TOKEN_PKIZ
+        if isinstance(revoked_token_pkiz, six.text_type):
+            revoked_token_pkiz = revoked_token_pkiz.encode('utf-8')
+        self.REVOKED_TOKEN_PKIZ_HASH = utils.hash_signed_token(
+            revoked_token_pkiz)
+        revoked_v3_token_pkiz = self.REVOKED_v3_TOKEN_PKIZ
+        if isinstance(revoked_v3_token_pkiz, six.text_type):
+            revoked_v3_token_pkiz = revoked_v3_token_pkiz.encode('utf-8')
+        self.REVOKED_v3_PKIZ_TOKEN_HASH = utils.hash_signed_token(
+            revoked_v3_token_pkiz)
+
+        self.REVOKED_TOKEN_PKIZ_LIST = (
+            {'revoked': [{'id': self.REVOKED_TOKEN_PKIZ_HASH,
+                          'expires': timeutils.utcnow()},
+                         {'id': self.REVOKED_v3_PKIZ_TOKEN_HASH,
+                          'expires': timeutils.utcnow()},
+                         ]})
+        self.REVOKED_TOKEN_PKIZ_LIST_JSON = jsonutils.dumps(
+            self.REVOKED_TOKEN_PKIZ_LIST)
+
+        self.SIGNED_TOKEN_SCOPED_KEY = cms.cms_hash_token(
+            self.SIGNED_TOKEN_SCOPED)
+        self.SIGNED_TOKEN_UNSCOPED_KEY = cms.cms_hash_token(
+            self.SIGNED_TOKEN_UNSCOPED)
+        self.SIGNED_v3_TOKEN_SCOPED_KEY = cms.cms_hash_token(
+            self.SIGNED_v3_TOKEN_SCOPED)
+
+        self.SIGNED_TOKEN_SCOPED_PKIZ_KEY = cms.cms_hash_token(
+            self.SIGNED_TOKEN_SCOPED_PKIZ)
+        self.SIGNED_TOKEN_UNSCOPED_PKIZ_KEY = cms.cms_hash_token(
+            self.SIGNED_TOKEN_UNSCOPED_PKIZ)
+        self.SIGNED_v3_TOKEN_SCOPED_PKIZ_KEY = cms.cms_hash_token(
+            self.SIGNED_v3_TOKEN_SCOPED_PKIZ)
+
+        self.INVALID_SIGNED_TOKEN = (
+            "MIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+            "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
+            "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
+            "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"
+            "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"
+            "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+            "0000000000000000000000000000000000000000000000000000000000000000"
+            "1111111111111111111111111111111111111111111111111111111111111111"
+            "2222222222222222222222222222222222222222222222222222222222222222"
+            "3333333333333333333333333333333333333333333333333333333333333333"
+            "4444444444444444444444444444444444444444444444444444444444444444"
+            "5555555555555555555555555555555555555555555555555555555555555555"
+            "6666666666666666666666666666666666666666666666666666666666666666"
+            "7777777777777777777777777777777777777777777777777777777777777777"
+            "8888888888888888888888888888888888888888888888888888888888888888"
+            "9999999999999999999999999999999999999999999999999999999999999999"
+            "0000000000000000000000000000000000000000000000000000000000000000")
+
+        self.INVALID_SIGNED_PKIZ_TOKEN = (
+            "PKIZ_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+            "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
+            "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
+            "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"
+            "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"
+            "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+            "0000000000000000000000000000000000000000000000000000000000000000"
+            "1111111111111111111111111111111111111111111111111111111111111111"
+            "2222222222222222222222222222222222222222222222222222222222222222"
+            "3333333333333333333333333333333333333333333333333333333333333333"
+            "4444444444444444444444444444444444444444444444444444444444444444"
+            "5555555555555555555555555555555555555555555555555555555555555555"
+            "6666666666666666666666666666666666666666666666666666666666666666"
+            "7777777777777777777777777777777777777777777777777777777777777777"
+            "8888888888888888888888888888888888888888888888888888888888888888"
+            "9999999999999999999999999999999999999999999999999999999999999999"
+            "0000000000000000000000000000000000000000000000000000000000000000")
+
+        # JSON responses keyed by token ID
+        self.TOKEN_RESPONSES = {}
+
+        # basic values
+        PROJECT_ID = 'tenant_id1'
+        PROJECT_NAME = 'tenant_name1'
+        USER_ID = 'user_id1'
+        USER_NAME = 'user_name1'
+        DOMAIN_ID = 'domain_id1'
+        DOMAIN_NAME = 'domain_name1'
+        ROLE_NAME1 = 'role1'
+        ROLE_NAME2 = 'role2'
+
+        SERVICE_PROJECT_ID = 'service_project_id1'
+        SERVICE_PROJECT_NAME = 'service_project_name1'
+        SERVICE_USER_ID = 'service_user_id1'
+        SERVICE_USER_NAME = 'service_user_name1'
+        SERVICE_DOMAIN_ID = 'service_domain_id1'
+        SERVICE_DOMAIN_NAME = 'service_domain_name1'
+        SERVICE_ROLE_NAME1 = 'service_role1'
+        SERVICE_ROLE_NAME2 = 'service_role2'
+
+        self.SERVICE_TYPE = 'identity'
+        self.UNVERSIONED_SERVICE_URL = 'http://keystone.server:5000/'
+        self.SERVICE_URL = self.UNVERSIONED_SERVICE_URL + 'v2.0'
+
+        # Old Tokens
+
+        self.TOKEN_RESPONSES[self.VALID_DIABLO_TOKEN] = {
+            'access': {
+                'token': {
+                    'id': self.VALID_DIABLO_TOKEN,
+                    'expires': '2020-01-01T00:00:10.000123Z',
+                    'tenantId': PROJECT_ID,
+                },
+                'user': {
+                    'id': USER_ID,
+                    'name': USER_NAME,
+                    'roles': [
+                        {'name': ROLE_NAME1},
+                        {'name': ROLE_NAME2},
+                    ],
+                },
+            },
+        }
+
+        # Generated V2 Tokens
+
+        token = fixture.V2Token(token_id=self.UUID_TOKEN_DEFAULT,
+                                tenant_id=PROJECT_ID,
+                                tenant_name=PROJECT_NAME,
+                                user_id=USER_ID,
+                                user_name=USER_NAME)
+        token.add_role(name=ROLE_NAME1)
+        token.add_role(name=ROLE_NAME2)
+        svc = token.add_service(self.SERVICE_TYPE)
+        svc.add_endpoint(public=self.SERVICE_URL)
+        self.TOKEN_RESPONSES[self.UUID_TOKEN_DEFAULT] = token
+
+        token = fixture.V2Token(token_id=self.UUID_TOKEN_UNSCOPED,
+                                user_id=USER_ID,
+                                user_name=USER_NAME)
+        self.TOKEN_RESPONSES[self.UUID_TOKEN_UNSCOPED] = token
+
+        token = fixture.V2Token(token_id='valid-token',
+                                tenant_id=PROJECT_ID,
+                                tenant_name=PROJECT_NAME,
+                                user_id=USER_ID,
+                                user_name=USER_NAME)
+        token.add_role(ROLE_NAME1)
+        token.add_role(ROLE_NAME2)
+        self.TOKEN_RESPONSES[self.UUID_TOKEN_NO_SERVICE_CATALOG] = token
+
+        token = fixture.V2Token(token_id=self.SIGNED_TOKEN_SCOPED_KEY,
+                                tenant_id=PROJECT_ID,
+                                tenant_name=PROJECT_NAME,
+                                user_id=USER_ID,
+                                user_name=USER_NAME)
+        token.add_role(ROLE_NAME1)
+        token.add_role(ROLE_NAME2)
+        self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_KEY] = token
+
+        token = fixture.V2Token(token_id=self.SIGNED_TOKEN_UNSCOPED_KEY,
+                                user_id=USER_ID,
+                                user_name=USER_NAME)
+        self.TOKEN_RESPONSES[self.SIGNED_TOKEN_UNSCOPED_KEY] = token
+
+        token = fixture.V2Token(token_id=self.UUID_TOKEN_BIND,
+                                tenant_id=PROJECT_ID,
+                                tenant_name=PROJECT_NAME,
+                                user_id=USER_ID,
+                                user_name=USER_NAME)
+        token.add_role(ROLE_NAME1)
+        token.add_role(ROLE_NAME2)
+        token['access']['token']['bind'] = {'kerberos': self.KERBEROS_BIND}
+        self.TOKEN_RESPONSES[self.UUID_TOKEN_BIND] = token
+
+        token = fixture.V2Token(token_id=self.UUID_TOKEN_UNKNOWN_BIND,
+                                tenant_id=PROJECT_ID,
+                                tenant_name=PROJECT_NAME,
+                                user_id=USER_ID,
+                                user_name=USER_NAME)
+        token.add_role(ROLE_NAME1)
+        token.add_role(ROLE_NAME2)
+        token['access']['token']['bind'] = {'FOO': 'BAR'}
+        self.TOKEN_RESPONSES[self.UUID_TOKEN_UNKNOWN_BIND] = token
+
+        token = fixture.V2Token(token_id=self.UUID_SERVICE_TOKEN_DEFAULT,
+                                tenant_id=SERVICE_PROJECT_ID,
+                                tenant_name=SERVICE_PROJECT_NAME,
+                                user_id=SERVICE_USER_ID,
+                                user_name=SERVICE_USER_NAME)
+        token.add_role(name=SERVICE_ROLE_NAME1)
+        token.add_role(name=SERVICE_ROLE_NAME2)
+        svc = token.add_service(self.SERVICE_TYPE)
+        svc.add_endpoint(public=self.SERVICE_URL)
+        self.TOKEN_RESPONSES[self.UUID_SERVICE_TOKEN_DEFAULT] = token
+
+        # Generated V3 Tokens
+
+        token = fixture.V3Token(user_id=USER_ID,
+                                user_name=USER_NAME,
+                                user_domain_id=DOMAIN_ID,
+                                user_domain_name=DOMAIN_NAME,
+                                project_id=PROJECT_ID,
+                                project_name=PROJECT_NAME,
+                                project_domain_id=DOMAIN_ID,
+                                project_domain_name=DOMAIN_NAME)
+        token.add_role(id=ROLE_NAME1, name=ROLE_NAME1)
+        token.add_role(id=ROLE_NAME2, name=ROLE_NAME2)
+        svc = token.add_service(self.SERVICE_TYPE)
+        svc.add_endpoint('public', self.SERVICE_URL)
+        self.TOKEN_RESPONSES[self.v3_UUID_TOKEN_DEFAULT] = token
+
+        token = fixture.V3Token(user_id=USER_ID,
+                                user_name=USER_NAME,
+                                user_domain_id=DOMAIN_ID,
+                                user_domain_name=DOMAIN_NAME)
+        self.TOKEN_RESPONSES[self.v3_UUID_TOKEN_UNSCOPED] = token
+
+        token = fixture.V3Token(user_id=USER_ID,
+                                user_name=USER_NAME,
+                                user_domain_id=DOMAIN_ID,
+                                user_domain_name=DOMAIN_NAME,
+                                domain_id=DOMAIN_ID,
+                                domain_name=DOMAIN_NAME)
+        token.add_role(id=ROLE_NAME1, name=ROLE_NAME1)
+        token.add_role(id=ROLE_NAME2, name=ROLE_NAME2)
+        svc = token.add_service(self.SERVICE_TYPE)
+        svc.add_endpoint('public', self.SERVICE_URL)
+        self.TOKEN_RESPONSES[self.v3_UUID_TOKEN_DOMAIN_SCOPED] = token
+
+        token = fixture.V3Token(user_id=USER_ID,
+                                user_name=USER_NAME,
+                                user_domain_id=DOMAIN_ID,
+                                user_domain_name=DOMAIN_NAME,
+                                project_id=PROJECT_ID,
+                                project_name=PROJECT_NAME,
+                                project_domain_id=DOMAIN_ID,
+                                project_domain_name=DOMAIN_NAME)
+        token.add_role(name=ROLE_NAME1)
+        token.add_role(name=ROLE_NAME2)
+        svc = token.add_service(self.SERVICE_TYPE)
+        svc.add_endpoint('public', self.SERVICE_URL)
+        self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_KEY] = token
+
+        token = fixture.V3Token(user_id=USER_ID,
+                                user_name=USER_NAME,
+                                user_domain_id=DOMAIN_ID,
+                                user_domain_name=DOMAIN_NAME,
+                                project_id=PROJECT_ID,
+                                project_name=PROJECT_NAME,
+                                project_domain_id=DOMAIN_ID,
+                                project_domain_name=DOMAIN_NAME)
+        token.add_role(name=ROLE_NAME1)
+        token.add_role(name=ROLE_NAME2)
+        svc = token.add_service(self.SERVICE_TYPE)
+        svc.add_endpoint('public', self.SERVICE_URL)
+        token['token']['bind'] = {'kerberos': self.KERBEROS_BIND}
+        self.TOKEN_RESPONSES[self.v3_UUID_TOKEN_BIND] = token
+
+        token = fixture.V3Token(user_id=USER_ID,
+                                user_name=USER_NAME,
+                                user_domain_id=DOMAIN_ID,
+                                user_domain_name=DOMAIN_NAME,
+                                project_id=PROJECT_ID,
+                                project_name=PROJECT_NAME,
+                                project_domain_id=DOMAIN_ID,
+                                project_domain_name=DOMAIN_NAME)
+        token.add_role(name=ROLE_NAME1)
+        token.add_role(name=ROLE_NAME2)
+        svc = token.add_service(self.SERVICE_TYPE)
+        svc.add_endpoint('public', self.SERVICE_URL)
+        token['token']['bind'] = {'FOO': 'BAR'}
+        self.TOKEN_RESPONSES[self.v3_UUID_TOKEN_UNKNOWN_BIND] = token
+
+        token = fixture.V3Token(user_id=SERVICE_USER_ID,
+                                user_name=SERVICE_USER_NAME,
+                                user_domain_id=SERVICE_DOMAIN_ID,
+                                user_domain_name=SERVICE_DOMAIN_NAME,
+                                project_id=SERVICE_PROJECT_ID,
+                                project_name=SERVICE_PROJECT_NAME,
+                                project_domain_id=SERVICE_DOMAIN_ID,
+                                project_domain_name=SERVICE_DOMAIN_NAME)
+        token.add_role(id=SERVICE_ROLE_NAME1,
+                       name=SERVICE_ROLE_NAME1)
+        token.add_role(id=SERVICE_ROLE_NAME2,
+                       name=SERVICE_ROLE_NAME2)
+        svc = token.add_service(self.SERVICE_TYPE)
+        svc.add_endpoint('public', self.SERVICE_URL)
+        self.TOKEN_RESPONSES[self.v3_UUID_SERVICE_TOKEN_DEFAULT] = token
+
+        # PKIZ tokens generally link to above tokens
+
+        self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_PKIZ_KEY] = (
+            self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_KEY])
+        self.TOKEN_RESPONSES[self.SIGNED_TOKEN_UNSCOPED_PKIZ_KEY] = (
+            self.TOKEN_RESPONSES[self.SIGNED_TOKEN_UNSCOPED_KEY])
+        self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_PKIZ_KEY] = (
+            self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_KEY])
+
+        self.JSON_TOKEN_RESPONSES = dict([(k, jsonutils.dumps(v)) for k, v in
+                                          six.iteritems(self.TOKEN_RESPONSES)])
+
+
+EXAMPLES_RESOURCE = testresources.FixtureResource(Examples())
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/test_audit_middleware.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/test_audit_middleware.py
new file mode 100644 (file)
index 0000000..89e5aa4
--- /dev/null
@@ -0,0 +1,485 @@
+#
+# 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.
+
+import os
+import tempfile
+import uuid
+
+import mock
+from oslo_config import cfg
+from pycadf import identifier
+import testtools
+from testtools import matchers
+import webob
+
+from keystonemiddleware import audit
+
+
+class FakeApp(object):
+    def __call__(self, env, start_response):
+        body = 'Some response'
+        start_response('200 OK', [
+            ('Content-Type', 'text/plain'),
+            ('Content-Length', str(sum(map(len, body))))
+        ])
+        return [body]
+
+
+class FakeFailingApp(object):
+    def __call__(self, env, start_response):
+        raise Exception('It happens!')
+
+
+class BaseAuditMiddlewareTest(testtools.TestCase):
+    def setUp(self):
+        super(BaseAuditMiddlewareTest, self).setUp()
+        self.fd, self.audit_map = tempfile.mkstemp()
+
+        with open(self.audit_map, "w") as f:
+            f.write("[custom_actions]\n")
+            f.write("reboot = start/reboot\n")
+            f.write("os-migrations/get = read\n\n")
+            f.write("[path_keywords]\n")
+            f.write("action = None\n")
+            f.write("os-hosts = host\n")
+            f.write("os-migrations = None\n")
+            f.write("reboot = None\n")
+            f.write("servers = server\n\n")
+            f.write("[service_endpoints]\n")
+            f.write("compute = service/compute")
+
+        cfg.CONF([], project='keystonemiddleware')
+
+        self.middleware = audit.AuditMiddleware(
+            FakeApp(), audit_map_file=self.audit_map,
+            service_name='pycadf')
+
+        self.addCleanup(lambda: os.close(self.fd))
+        self.addCleanup(cfg.CONF.reset)
+
+    @staticmethod
+    def get_environ_header(req_type):
+        env_headers = {'HTTP_X_SERVICE_CATALOG':
+                       '''[{"endpoints_links": [],
+                            "endpoints": [{"adminURL":
+                                           "http://admin_host:8774",
+                                           "region": "RegionOne",
+                                           "publicURL":
+                                           "http://public_host:8774",
+                                           "internalURL":
+                                           "http://internal_host:8774",
+                                           "id": "resource_id"}],
+                           "type": "compute",
+                           "name": "nova"},]''',
+                       'HTTP_X_USER_ID': 'user_id',
+                       'HTTP_X_USER_NAME': 'user_name',
+                       'HTTP_X_AUTH_TOKEN': 'token',
+                       'HTTP_X_PROJECT_ID': 'tenant_id',
+                       'HTTP_X_IDENTITY_STATUS': 'Confirmed'}
+        env_headers['REQUEST_METHOD'] = req_type
+        return env_headers
+
+
+@mock.patch('oslo.messaging.get_transport', mock.MagicMock())
+class AuditMiddlewareTest(BaseAuditMiddlewareTest):
+
+    def test_api_request(self):
+        req = webob.Request.blank('/foo/bar',
+                                  environ=self.get_environ_header('GET'))
+        with mock.patch('oslo.messaging.Notifier.info') as notify:
+            self.middleware(req)
+            # Check first notification with only 'request'
+            call_args = notify.call_args_list[0][0]
+            self.assertEqual('audit.http.request', call_args[1])
+            self.assertEqual('/foo/bar', call_args[2]['requestPath'])
+            self.assertEqual('pending', call_args[2]['outcome'])
+            self.assertNotIn('reason', call_args[2])
+            self.assertNotIn('reporterchain', call_args[2])
+
+            # Check second notification with request + response
+            call_args = notify.call_args_list[1][0]
+            self.assertEqual('audit.http.response', call_args[1])
+            self.assertEqual('/foo/bar', call_args[2]['requestPath'])
+            self.assertEqual('success', call_args[2]['outcome'])
+            self.assertIn('reason', call_args[2])
+            self.assertIn('reporterchain', call_args[2])
+
+    def test_api_request_failure(self):
+        self.middleware = audit.AuditMiddleware(
+            FakeFailingApp(),
+            audit_map_file=self.audit_map,
+            service_name='pycadf')
+        req = webob.Request.blank('/foo/bar',
+                                  environ=self.get_environ_header('GET'))
+        with mock.patch('oslo.messaging.Notifier.info') as notify:
+            try:
+                self.middleware(req)
+                self.fail('Application exception has not been re-raised')
+            except Exception:
+                pass
+            # Check first notification with only 'request'
+            call_args = notify.call_args_list[0][0]
+            self.assertEqual('audit.http.request', call_args[1])
+            self.assertEqual('/foo/bar', call_args[2]['requestPath'])
+            self.assertEqual('pending', call_args[2]['outcome'])
+            self.assertNotIn('reporterchain', call_args[2])
+
+            # Check second notification with request + response
+            call_args = notify.call_args_list[1][0]
+            self.assertEqual('audit.http.response', call_args[1])
+            self.assertEqual('/foo/bar', call_args[2]['requestPath'])
+            self.assertEqual('unknown', call_args[2]['outcome'])
+            self.assertIn('reporterchain', call_args[2])
+
+    def test_process_request_fail(self):
+        req = webob.Request.blank('/foo/bar',
+                                  environ=self.get_environ_header('GET'))
+        with mock.patch('oslo.messaging.Notifier.info',
+                        side_effect=Exception('error')) as notify:
+            self.middleware._process_request(req)
+            self.assertTrue(notify.called)
+
+    def test_process_response_fail(self):
+        req = webob.Request.blank('/foo/bar',
+                                  environ=self.get_environ_header('GET'))
+        with mock.patch('oslo.messaging.Notifier.info',
+                        side_effect=Exception('error')) as notify:
+            self.middleware._process_response(req, webob.response.Response())
+            self.assertTrue(notify.called)
+
+    def test_ignore_req_opt(self):
+        self.middleware = audit.AuditMiddleware(FakeApp(),
+                                                audit_map_file=self.audit_map,
+                                                ignore_req_list='get, PUT')
+        req = webob.Request.blank('/skip/foo',
+                                  environ=self.get_environ_header('GET'))
+        req1 = webob.Request.blank('/skip/foo',
+                                   environ=self.get_environ_header('PUT'))
+        req2 = webob.Request.blank('/accept/foo',
+                                   environ=self.get_environ_header('POST'))
+        with mock.patch('oslo.messaging.Notifier.info') as notify:
+            # Check GET/PUT request does not send notification
+            self.middleware(req)
+            self.middleware(req1)
+            self.assertEqual([], notify.call_args_list)
+
+            # Check non-GET/PUT request does send notification
+            self.middleware(req2)
+            self.assertThat(notify.call_args_list, matchers.HasLength(2))
+            call_args = notify.call_args_list[0][0]
+            self.assertEqual('audit.http.request', call_args[1])
+            self.assertEqual('/accept/foo', call_args[2]['requestPath'])
+
+            call_args = notify.call_args_list[1][0]
+            self.assertEqual('audit.http.response', call_args[1])
+            self.assertEqual('/accept/foo', call_args[2]['requestPath'])
+
+    def test_api_request_no_messaging(self):
+        req = webob.Request.blank('/foo/bar',
+                                  environ=self.get_environ_header('GET'))
+        with mock.patch('keystonemiddleware.audit.messaging', None):
+            with mock.patch('keystonemiddleware.audit._LOG.info') as log:
+                self.middleware(req)
+                # Check first notification with only 'request'
+                call_args = log.call_args_list[0][0]
+                self.assertEqual('audit.http.request',
+                                 call_args[1]['event_type'])
+
+                # Check second notification with request + response
+                call_args = log.call_args_list[1][0]
+                self.assertEqual('audit.http.response',
+                                 call_args[1]['event_type'])
+
+    def test_cadf_event_scoped_to_request(self):
+        middleware = audit.AuditMiddleware(
+            FakeApp(),
+            audit_map_file=self.audit_map,
+            service_name='pycadf')
+        req = webob.Request.blank('/foo/bar',
+                                  environ=self.get_environ_header('GET'))
+        with mock.patch('oslo.messaging.Notifier.info') as notify:
+            middleware(req)
+            self.assertIsNotNone(req.environ.get('cadf_event'))
+
+            # ensure exact same event is used between request and response
+            self.assertEqual(notify.call_args_list[0][0][2]['id'],
+                             notify.call_args_list[1][0][2]['id'])
+
+    def test_cadf_event_scoped_to_request_on_error(self):
+        middleware = audit.AuditMiddleware(
+            FakeApp(),
+            audit_map_file=self.audit_map,
+            service_name='pycadf')
+        req = webob.Request.blank('/foo/bar',
+                                  environ=self.get_environ_header('GET'))
+        with mock.patch('oslo.messaging.Notifier.info',
+                        side_effect=Exception('error')) as notify:
+            middleware._process_request(req)
+            self.assertTrue(notify.called)
+        req2 = webob.Request.blank('/foo/bar',
+                                   environ=self.get_environ_header('GET'))
+        with mock.patch('oslo.messaging.Notifier.info') as notify:
+            middleware._process_response(req2, webob.response.Response())
+            self.assertTrue(notify.called)
+            # ensure event is not the same across requests
+            self.assertNotEqual(req.environ['cadf_event'].id,
+                                notify.call_args_list[0][0][2]['id'])
+
+
+@mock.patch('oslo.messaging', mock.MagicMock())
+class AuditApiLogicTest(BaseAuditMiddlewareTest):
+
+    def api_request(self, method, url):
+        req = webob.Request.blank(url, environ=self.get_environ_header(method),
+                                  remote_addr='192.168.0.1')
+        self.middleware._process_request(req)
+        return req
+
+    def test_get_list(self):
+        req = self.api_request('GET', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['action'], 'read/list')
+        self.assertEqual(payload['typeURI'],
+                         'http://schemas.dmtf.org/cloud/audit/1.0/event')
+        self.assertEqual(payload['outcome'], 'pending')
+        self.assertEqual(payload['eventType'], 'activity')
+        self.assertEqual(payload['target']['name'], 'nova')
+        self.assertEqual(payload['target']['id'], 'openstack:resource_id')
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/servers')
+        self.assertEqual(len(payload['target']['addresses']), 3)
+        self.assertEqual(payload['target']['addresses'][0]['name'], 'admin')
+        self.assertEqual(payload['target']['addresses'][0]['url'],
+                         'http://admin_host:8774')
+        self.assertEqual(payload['initiator']['id'], 'openstack:user_id')
+        self.assertEqual(payload['initiator']['name'], 'user_name')
+        self.assertEqual(payload['initiator']['project_id'],
+                         'openstack:tenant_id')
+        self.assertEqual(payload['initiator']['host']['address'],
+                         '192.168.0.1')
+        self.assertEqual(payload['initiator']['typeURI'],
+                         'service/security/account/user')
+        self.assertNotEqual(payload['initiator']['credential']['token'],
+                            'token')
+        self.assertEqual(payload['initiator']['credential']['identity_status'],
+                         'Confirmed')
+        self.assertNotIn('reason', payload)
+        self.assertNotIn('reporterchain', payload)
+        self.assertEqual(payload['observer']['id'], 'target')
+        self.assertEqual(req.path, payload['requestPath'])
+
+    def test_get_read(self):
+        req = self.api_request('GET', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers/'
+                               + str(uuid.uuid4()))
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/servers/server')
+        self.assertEqual(payload['action'], 'read')
+        self.assertEqual(payload['outcome'], 'pending')
+
+    def test_get_unknown_endpoint(self):
+        req = self.api_request('GET', 'http://unknown:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['action'], 'read/list')
+        self.assertEqual(payload['outcome'], 'pending')
+        self.assertEqual(payload['target']['name'], 'unknown')
+        self.assertEqual(payload['target']['id'], 'unknown')
+        self.assertEqual(payload['target']['typeURI'], 'unknown')
+
+    def test_get_unknown_endpoint_default_set(self):
+        with open(self.audit_map, "w") as f:
+            f.write("[DEFAULT]\n")
+            f.write("target_endpoint_type = compute\n")
+            f.write("[path_keywords]\n")
+            f.write("servers = server\n\n")
+            f.write("[service_endpoints]\n")
+            f.write("compute = service/compute")
+
+        self.middleware = audit.AuditMiddleware(
+            FakeApp(), audit_map_file=self.audit_map,
+            service_name='pycadf')
+
+        req = self.api_request('GET', 'http://unknown:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['action'], 'read/list')
+        self.assertEqual(payload['outcome'], 'pending')
+        self.assertEqual(payload['target']['name'], 'nova')
+        self.assertEqual(payload['target']['id'], 'openstack:resource_id')
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/servers')
+
+    def test_put(self):
+        req = self.api_request('PUT', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/servers')
+        self.assertEqual(payload['action'], 'update')
+        self.assertEqual(payload['outcome'], 'pending')
+
+    def test_delete(self):
+        req = self.api_request('DELETE', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/servers')
+        self.assertEqual(payload['action'], 'delete')
+        self.assertEqual(payload['outcome'], 'pending')
+
+    def test_head(self):
+        req = self.api_request('HEAD', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/servers')
+        self.assertEqual(payload['action'], 'read')
+        self.assertEqual(payload['outcome'], 'pending')
+
+    def test_post_update(self):
+        req = self.api_request('POST',
+                               'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers/'
+                               + str(uuid.uuid4()))
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/servers/server')
+        self.assertEqual(payload['action'], 'update')
+        self.assertEqual(payload['outcome'], 'pending')
+
+    def test_post_create(self):
+        req = self.api_request('POST', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/servers')
+        self.assertEqual(payload['action'], 'create')
+        self.assertEqual(payload['outcome'], 'pending')
+
+    def test_post_action(self):
+        req = webob.Request.blank('http://admin_host:8774/v2/'
+                                  + str(uuid.uuid4()) + '/servers/action',
+                                  environ=self.get_environ_header('POST'))
+        req.body = b'{"createImage" : {"name" : "new-image","metadata": ' \
+                   b'{"ImageType": "Gold","ImageVersion": "2.0"}}}'
+        self.middleware._process_request(req)
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/servers/action')
+        self.assertEqual(payload['action'], 'update/createImage')
+        self.assertEqual(payload['outcome'], 'pending')
+
+    def test_post_empty_body_action(self):
+        req = self.api_request('POST', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers/action')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/servers/action')
+        self.assertEqual(payload['action'], 'create')
+        self.assertEqual(payload['outcome'], 'pending')
+
+    def test_custom_action(self):
+        req = self.api_request('GET', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/os-hosts/'
+                               + str(uuid.uuid4()) + '/reboot')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/os-hosts/host/reboot')
+        self.assertEqual(payload['action'], 'start/reboot')
+        self.assertEqual(payload['outcome'], 'pending')
+
+    def test_custom_action_complex(self):
+        req = self.api_request('GET', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/os-migrations')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/os-migrations')
+        self.assertEqual(payload['action'], 'read')
+        req = self.api_request('POST', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/os-migrations')
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['typeURI'],
+                         'service/compute/os-migrations')
+        self.assertEqual(payload['action'], 'create')
+
+    def test_response_mod_msg(self):
+        req = self.api_request('GET', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers')
+        payload = req.environ['cadf_event'].as_dict()
+        self.middleware._process_response(req, webob.Response())
+        payload2 = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['id'], payload2['id'])
+        self.assertEqual(payload['tags'], payload2['tags'])
+        self.assertEqual(payload2['outcome'], 'success')
+        self.assertEqual(payload2['reason']['reasonType'], 'HTTP')
+        self.assertEqual(payload2['reason']['reasonCode'], '200')
+        self.assertEqual(len(payload2['reporterchain']), 1)
+        self.assertEqual(payload2['reporterchain'][0]['role'], 'modifier')
+        self.assertEqual(payload2['reporterchain'][0]['reporter']['id'],
+                         'target')
+
+    def test_no_response(self):
+        req = self.api_request('GET', 'http://admin_host:8774/v2/'
+                               + str(uuid.uuid4()) + '/servers')
+        payload = req.environ['cadf_event'].as_dict()
+        self.middleware._process_response(req, None)
+        payload2 = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['id'], payload2['id'])
+        self.assertEqual(payload['tags'], payload2['tags'])
+        self.assertEqual(payload2['outcome'], 'unknown')
+        self.assertNotIn('reason', payload2)
+        self.assertEqual(len(payload2['reporterchain']), 1)
+        self.assertEqual(payload2['reporterchain'][0]['role'], 'modifier')
+        self.assertEqual(payload2['reporterchain'][0]['reporter']['id'],
+                         'target')
+
+    def test_missing_req(self):
+        req = webob.Request.blank('http://admin_host:8774/v2/'
+                                  + str(uuid.uuid4()) + '/servers',
+                                  environ=self.get_environ_header('GET'))
+        self.assertNotIn('cadf_event', req.environ)
+        self.middleware._process_response(req, webob.Response())
+        self.assertIn('cadf_event', req.environ)
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['outcome'], 'success')
+        self.assertEqual(payload['reason']['reasonType'], 'HTTP')
+        self.assertEqual(payload['reason']['reasonCode'], '200')
+        self.assertEqual(payload['observer']['id'], 'target')
+
+    def test_missing_catalog_endpoint_id(self):
+        env_headers = {'HTTP_X_SERVICE_CATALOG':
+                       '''[{"endpoints_links": [],
+                            "endpoints": [{"adminURL":
+                                           "http://admin_host:8774",
+                                           "region": "RegionOne",
+                                           "publicURL":
+                                           "http://public_host:8774",
+                                           "internalURL":
+                                           "http://internal_host:8774"}],
+                           "type": "compute",
+                           "name": "nova"},]''',
+                       'HTTP_X_USER_ID': 'user_id',
+                       'HTTP_X_USER_NAME': 'user_name',
+                       'HTTP_X_AUTH_TOKEN': 'token',
+                       'HTTP_X_PROJECT_ID': 'tenant_id',
+                       'HTTP_X_IDENTITY_STATUS': 'Confirmed',
+                       'REQUEST_METHOD': 'GET'}
+        req = webob.Request.blank('http://admin_host:8774/v2/'
+                                  + str(uuid.uuid4()) + '/servers',
+                                  environ=env_headers)
+        self.middleware._process_request(req)
+        payload = req.environ['cadf_event'].as_dict()
+        self.assertEqual(payload['target']['id'], identifier.norm_ns('nova'))
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/test_opts.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/test_opts.py
new file mode 100644 (file)
index 0000000..93e1b06
--- /dev/null
@@ -0,0 +1,85 @@
+# Copyright (c) 2014 OpenStack Foundation.
+#
+# 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.
+
+import pkg_resources
+from testtools import matchers
+
+from keystonemiddleware import opts
+from keystonemiddleware.tests.unit import utils
+
+
+class OptsTestCase(utils.TestCase):
+
+    def _test_list_auth_token_opts(self, result):
+        self.assertThat(result, matchers.HasLength(1))
+
+        for group in (g for (g, _l) in result):
+            self.assertEqual('keystone_authtoken', group)
+
+        expected_opt_names = [
+            'auth_admin_prefix',
+            'auth_host',
+            'auth_port',
+            'auth_protocol',
+            'auth_uri',
+            'identity_uri',
+            'auth_version',
+            'delay_auth_decision',
+            'http_connect_timeout',
+            'http_request_max_retries',
+            'admin_token',
+            'admin_user',
+            'admin_password',
+            'admin_tenant_name',
+            'cache',
+            'certfile',
+            'keyfile',
+            'cafile',
+            'insecure',
+            'signing_dir',
+            'memcached_servers',
+            'token_cache_time',
+            'revocation_cache_time',
+            'memcache_security_strategy',
+            'memcache_secret_key',
+            'memcache_use_advanced_pool',
+            'memcache_pool_dead_retry',
+            'memcache_pool_maxsize',
+            'memcache_pool_unused_timeout',
+            'memcache_pool_conn_get_timeout',
+            'memcache_pool_socket_timeout',
+            'include_service_catalog',
+            'enforce_token_bind',
+            'check_revocations_for_cached',
+            'hash_algorithms'
+        ]
+        opt_names = [o.name for (g, l) in result for o in l]
+        self.assertThat(opt_names, matchers.HasLength(len(expected_opt_names)))
+
+        for opt in opt_names:
+            self.assertIn(opt, expected_opt_names)
+
+    def test_list_auth_token_opts(self):
+        self._test_list_auth_token_opts(opts.list_auth_token_opts())
+
+    def test_entry_point(self):
+        result = None
+        for ep in pkg_resources.iter_entry_points('oslo.config.opts'):
+            if ep.name == 'keystonemiddleware.auth_token':
+                list_fn = ep.load()
+                result = list_fn()
+                break
+
+        self.assertIsNotNone(result)
+        self._test_list_auth_token_opts(result)
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/test_s3_token_middleware.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/test_s3_token_middleware.py
new file mode 100644 (file)
index 0000000..2bcdf89
--- /dev/null
@@ -0,0 +1,235 @@
+# Copyright 2012 OpenStack Foundation
+#
+# 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.
+
+import mock
+from oslo_serialization import jsonutils
+import requests
+from requests_mock.contrib import fixture as rm_fixture
+import six
+import testtools
+import webob
+
+from keystonemiddleware import s3_token
+from keystonemiddleware.tests.unit import utils
+
+
+GOOD_RESPONSE = {'access': {'token': {'id': 'TOKEN_ID',
+                                      'tenant': {'id': 'TENANT_ID'}}}}
+
+
+class FakeApp(object):
+    """This represents a WSGI app protected by the auth_token middleware."""
+    def __call__(self, env, start_response):
+        resp = webob.Response()
+        resp.environ = env
+        return resp(env, start_response)
+
+
+class S3TokenMiddlewareTestBase(utils.TestCase):
+
+    TEST_PROTOCOL = 'https'
+    TEST_HOST = 'fakehost'
+    TEST_PORT = 35357
+    TEST_URL = '%s://%s:%d/v2.0/s3tokens' % (TEST_PROTOCOL,
+                                             TEST_HOST,
+                                             TEST_PORT)
+
+    def setUp(self):
+        super(S3TokenMiddlewareTestBase, self).setUp()
+
+        self.conf = {
+            'auth_host': self.TEST_HOST,
+            'auth_port': self.TEST_PORT,
+            'auth_protocol': self.TEST_PROTOCOL,
+        }
+
+        self.requests = self.useFixture(rm_fixture.Fixture())
+
+    def start_fake_response(self, status, headers):
+        self.response_status = int(status.split(' ', 1)[0])
+        self.response_headers = dict(headers)
+
+
+class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
+
+    def setUp(self):
+        super(S3TokenMiddlewareTestGood, self).setUp()
+        self.middleware = s3_token.S3Token(FakeApp(), self.conf)
+
+        self.requests.post(self.TEST_URL, status_code=201, json=GOOD_RESPONSE)
+
+    # Ignore the request and pass to the next middleware in the
+    # pipeline if no path has been specified.
+    def test_no_path_request(self):
+        req = webob.Request.blank('/')
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 200)
+
+    # Ignore the request and pass to the next middleware in the
+    # pipeline if no Authorization header has been specified
+    def test_without_authorization(self):
+        req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 200)
+
+    def test_without_auth_storage_token(self):
+        req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+        req.headers['Authorization'] = 'badboy'
+        self.middleware(req.environ, self.start_fake_response)
+        self.assertEqual(self.response_status, 200)
+
+    def test_authorized(self):
+        req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+        req.headers['Authorization'] = 'access:signature'
+        req.headers['X-Storage-Token'] = 'token'
+        req.get_response(self.middleware)
+        self.assertTrue(req.path.startswith('/v1/AUTH_TENANT_ID'))
+        self.assertEqual(req.headers['X-Auth-Token'], 'TOKEN_ID')
+
+    def test_authorized_http(self):
+        self.requests.post(self.TEST_URL.replace('https', 'http'),
+                           status_code=201,
+                           json=GOOD_RESPONSE)
+
+        self.middleware = (
+            s3_token.filter_factory({'auth_protocol': 'http',
+                                     'auth_host': self.TEST_HOST,
+                                     'auth_port': self.TEST_PORT})(FakeApp()))
+        req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+        req.headers['Authorization'] = 'access:signature'
+        req.headers['X-Storage-Token'] = 'token'
+        req.get_response(self.middleware)
+        self.assertTrue(req.path.startswith('/v1/AUTH_TENANT_ID'))
+        self.assertEqual(req.headers['X-Auth-Token'], 'TOKEN_ID')
+
+    def test_authorization_nova_toconnect(self):
+        req = webob.Request.blank('/v1/AUTH_swiftint/c/o')
+        req.headers['Authorization'] = 'access:FORCED_TENANT_ID:signature'
+        req.headers['X-Storage-Token'] = 'token'
+        req.get_response(self.middleware)
+        path = req.environ['PATH_INFO']
+        self.assertTrue(path.startswith('/v1/AUTH_FORCED_TENANT_ID'))
+
+    @mock.patch.object(requests, 'post')
+    def test_insecure(self, MOCK_REQUEST):
+        self.middleware = (
+            s3_token.filter_factory({'insecure': True})(FakeApp()))
+
+        text_return_value = jsonutils.dumps(GOOD_RESPONSE)
+        if six.PY3:
+            text_return_value = text_return_value.encode()
+        MOCK_REQUEST.return_value = utils.TestResponse({
+            'status_code': 201,
+            'text': text_return_value})
+
+        req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+        req.headers['Authorization'] = 'access:signature'
+        req.headers['X-Storage-Token'] = 'token'
+        req.get_response(self.middleware)
+
+        self.assertTrue(MOCK_REQUEST.called)
+        mock_args, mock_kwargs = MOCK_REQUEST.call_args
+        self.assertIs(mock_kwargs['verify'], False)
+
+
+class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase):
+    def setUp(self):
+        super(S3TokenMiddlewareTestBad, self).setUp()
+        self.middleware = s3_token.S3Token(FakeApp(), self.conf)
+
+    def test_unauthorized_token(self):
+        ret = {"error":
+               {"message": "EC2 access key not found.",
+                "code": 401,
+                "title": "Unauthorized"}}
+        self.requests.post(self.TEST_URL, status_code=403, json=ret)
+        req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+        req.headers['Authorization'] = 'access:signature'
+        req.headers['X-Storage-Token'] = 'token'
+        resp = req.get_response(self.middleware)
+        s3_denied_req = self.middleware._deny_request('AccessDenied')
+        self.assertEqual(resp.body, s3_denied_req.body)
+        self.assertEqual(resp.status_int, s3_denied_req.status_int)
+
+    def test_bogus_authorization(self):
+        req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+        req.headers['Authorization'] = 'badboy'
+        req.headers['X-Storage-Token'] = 'token'
+        resp = req.get_response(self.middleware)
+        self.assertEqual(resp.status_int, 400)
+        s3_invalid_req = self.middleware._deny_request('InvalidURI')
+        self.assertEqual(resp.body, s3_invalid_req.body)
+        self.assertEqual(resp.status_int, s3_invalid_req.status_int)
+
+    def test_fail_to_connect_to_keystone(self):
+        with mock.patch.object(self.middleware, '_json_request') as o:
+            s3_invalid_req = self.middleware._deny_request('InvalidURI')
+            o.side_effect = s3_token.ServiceError(s3_invalid_req)
+
+            req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+            req.headers['Authorization'] = 'access:signature'
+            req.headers['X-Storage-Token'] = 'token'
+            resp = req.get_response(self.middleware)
+            self.assertEqual(resp.body, s3_invalid_req.body)
+            self.assertEqual(resp.status_int, s3_invalid_req.status_int)
+
+    def test_bad_reply(self):
+        self.requests.post(self.TEST_URL, status_code=201, text="<badreply>")
+
+        req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+        req.headers['Authorization'] = 'access:signature'
+        req.headers['X-Storage-Token'] = 'token'
+        resp = req.get_response(self.middleware)
+        s3_invalid_req = self.middleware._deny_request('InvalidURI')
+        self.assertEqual(resp.body, s3_invalid_req.body)
+        self.assertEqual(resp.status_int, s3_invalid_req.status_int)
+
+
+class S3TokenMiddlewareTestUtil(testtools.TestCase):
+    def test_split_path_failed(self):
+        self.assertRaises(ValueError, s3_token._split_path, '')
+        self.assertRaises(ValueError, s3_token._split_path, '/')
+        self.assertRaises(ValueError, s3_token._split_path, '//')
+        self.assertRaises(ValueError, s3_token._split_path, '//a')
+        self.assertRaises(ValueError, s3_token._split_path, '/a/c')
+        self.assertRaises(ValueError, s3_token._split_path, '//c')
+        self.assertRaises(ValueError, s3_token._split_path, '/a/c/')
+        self.assertRaises(ValueError, s3_token._split_path, '/a//')
+        self.assertRaises(ValueError, s3_token._split_path, '/a', 2)
+        self.assertRaises(ValueError, s3_token._split_path, '/a', 2, 3)
+        self.assertRaises(ValueError, s3_token._split_path, '/a', 2, 3, True)
+        self.assertRaises(ValueError, s3_token._split_path, '/a/c/o/r', 3, 3)
+        self.assertRaises(ValueError, s3_token._split_path, '/a', 5, 4)
+
+    def test_split_path_success(self):
+        self.assertEqual(s3_token._split_path('/a'), ['a'])
+        self.assertEqual(s3_token._split_path('/a/'), ['a'])
+        self.assertEqual(s3_token._split_path('/a/c', 2), ['a', 'c'])
+        self.assertEqual(s3_token._split_path('/a/c/o', 3), ['a', 'c', 'o'])
+        self.assertEqual(s3_token._split_path('/a/c/o/r', 3, 3, True),
+                         ['a', 'c', 'o/r'])
+        self.assertEqual(s3_token._split_path('/a/c', 2, 3, True),
+                         ['a', 'c', None])
+        self.assertEqual(s3_token._split_path('/a/c/', 2), ['a', 'c'])
+        self.assertEqual(s3_token._split_path('/a/c/', 2, 3), ['a', 'c', ''])
+
+    def test_split_path_invalid_path(self):
+        try:
+            s3_token._split_path('o\nn e', 2)
+        except ValueError as err:
+            self.assertEqual(str(err), 'Invalid path: o%0An%20e')
+        try:
+            s3_token._split_path('o\nn e', 2, 3, True)
+        except ValueError as err:
+            self.assertEqual(str(err), 'Invalid path: o%0An%20e')
diff --git a/keystonemiddleware-moon/keystonemiddleware/tests/unit/utils.py b/keystonemiddleware-moon/keystonemiddleware/tests/unit/utils.py
new file mode 100644 (file)
index 0000000..da6f347
--- /dev/null
@@ -0,0 +1,138 @@
+#    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.
+
+import logging
+import sys
+import time
+
+import fixtures
+import mock
+import requests
+import testtools
+import uuid
+
+
+class TestCase(testtools.TestCase):
+    TEST_DOMAIN_ID = '1'
+    TEST_DOMAIN_NAME = 'aDomain'
+    TEST_GROUP_ID = uuid.uuid4().hex
+    TEST_ROLE_ID = uuid.uuid4().hex
+    TEST_TENANT_ID = '1'
+    TEST_TENANT_NAME = 'aTenant'
+    TEST_TOKEN = 'aToken'
+    TEST_TRUST_ID = 'aTrust'
+    TEST_USER = 'test'
+    TEST_USER_ID = uuid.uuid4().hex
+
+    TEST_ROOT_URL = 'http://127.0.0.1:5000/'
+
+    def setUp(self):
+        super(TestCase, self).setUp()
+        self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG))
+        self.time_patcher = mock.patch.object(time, 'time', lambda: 1234)
+        self.time_patcher.start()
+
+    def tearDown(self):
+        self.time_patcher.stop()
+        super(TestCase, self).tearDown()
+
+
+if tuple(sys.version_info)[0:2] < (2, 7):
+
+    def assertDictEqual(self, d1, d2, msg=None):
+        # Simple version taken from 2.7
+        self.assertIsInstance(d1, dict,
+                              'First argument is not a dictionary')
+        self.assertIsInstance(d2, dict,
+                              'Second argument is not a dictionary')
+        if d1 != d2:
+            if msg:
+                self.fail(msg)
+            else:
+                standardMsg = '%r != %r' % (d1, d2)
+                self.fail(standardMsg)
+
+    TestCase.assertDictEqual = assertDictEqual
+
+
+class TestResponse(requests.Response):
+    """Class used to wrap requests.Response and provide some
+       convenience to initialize with a dict.
+    """
+
+    def __init__(self, data):
+        self._text = None
+        super(TestResponse, self).__init__()
+        if isinstance(data, dict):
+            self.status_code = data.get('status_code', 200)
+            headers = data.get('headers')
+            if headers:
+                self.headers.update(headers)
+            # Fake the text attribute to streamline Response creation
+            # _content is defined by requests.Response
+            self._content = data.get('text')
+        else:
+            self.status_code = data
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    @property
+    def text(self):
+        return self.content
+
+
+class DisableModuleFixture(fixtures.Fixture):
+    """A fixture to provide support for unloading/disabling modules."""
+
+    def __init__(self, module, *args, **kw):
+        super(DisableModuleFixture, self).__init__(*args, **kw)
+        self.module = module
+        self._finders = []
+        self._cleared_modules = {}
+
+    def tearDown(self):
+        super(DisableModuleFixture, self).tearDown()
+        for finder in self._finders:
+            sys.meta_path.remove(finder)
+        sys.modules.update(self._cleared_modules)
+
+    def clear_module(self):
+        cleared_modules = {}
+        for fullname in sys.modules.keys():
+            if (fullname == self.module or
+                    fullname.startswith(self.module + '.')):
+                cleared_modules[fullname] = sys.modules.pop(fullname)
+        return cleared_modules
+
+    def setUp(self):
+        """Ensure ImportError for the specified module."""
+
+        super(DisableModuleFixture, self).setUp()
+
+        # Clear 'module' references in sys.modules
+        self._cleared_modules.update(self.clear_module())
+
+        finder = NoModuleFinder(self.module)
+        self._finders.append(finder)
+        sys.meta_path.insert(0, finder)
+
+
+class NoModuleFinder(object):
+    """Disallow further imports of 'module'."""
+
+    def __init__(self, module):
+        self.module = module
+
+    def find_module(self, fullname, path):
+        if fullname == self.module or fullname.startswith(self.module + '.'):
+            raise ImportError
diff --git a/keystonemiddleware-moon/openstack-common.conf b/keystonemiddleware-moon/openstack-common.conf
new file mode 100644 (file)
index 0000000..7bac626
--- /dev/null
@@ -0,0 +1,8 @@
+[DEFAULT]
+
+# The list of modules to copy from oslo-incubator
+module=install_venv_common
+module=memorycache
+
+# The base module to hold the copy of openstack.common
+base=keystonemiddleware
diff --git a/keystonemiddleware-moon/requirements.txt b/keystonemiddleware-moon/requirements.txt
new file mode 100644 (file)
index 0000000..b207833
--- /dev/null
@@ -0,0 +1,17 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+
+Babel>=1.3
+iso8601>=0.1.9
+oslo.config>=1.9.0  # Apache-2.0
+oslo.context>=0.2.0                     # Apache-2.0
+oslo.i18n>=1.3.0  # Apache-2.0
+oslo.serialization>=1.2.0               # Apache-2.0
+oslo.utils>=1.2.0                       # Apache-2.0
+pbr>=0.6,!=0.7,<1.0
+pycadf>=0.8.0
+python-keystoneclient>=1.1.0
+requests>=2.2.0,!=2.4.0
+six>=1.9.0
+WebOb>=1.2.3
diff --git a/keystonemiddleware-moon/setup.cfg b/keystonemiddleware-moon/setup.cfg
new file mode 100644 (file)
index 0000000..5cc3067
--- /dev/null
@@ -0,0 +1,57 @@
+[metadata]
+name = keystonemiddleware
+summary = Middleware for OpenStack Identity
+description-file =
+    README.rst
+author = OpenStack
+author-email = openstack-dev@lists.openstack.org
+home-page = http://launchpad.net/keystonemiddleware
+classifier =
+    Environment :: OpenStack
+    Intended Audience :: Information Technology
+    Intended Audience :: System Administrators
+    License :: OSI Approved :: Apache Software License
+    Operating System :: POSIX :: Linux
+    Programming Language :: Python
+    Programming Language :: Python :: 2
+    Programming Language :: Python :: 2.7
+    Programming Language :: Python :: 2.6
+    Programming Language :: Python :: 3
+    Programming Language :: Python :: 3.3
+
+[files]
+packages =
+    keystonemiddleware
+
+[global]
+setup-hooks =
+    pbr.hooks.setup_hook
+
+[entry_points]
+oslo.config.opts =
+    keystonemiddleware.auth_token = keystonemiddleware.opts:list_auth_token_opts
+
+[build_sphinx]
+source-dir = doc/source
+build-dir = doc/build
+all_files = 1
+
+[upload_sphinx]
+upload-dir = doc/build/html
+
+[compile_catalog]
+directory = keystonemiddleware/locale
+domain = keystonemiddleware
+
+[update_catalog]
+domain = keystonemiddleware
+output_dir = keystonemiddleware/locale
+input_file = keystonemiddleware/locale/keystonemiddleware.pot
+
+[extract_messages]
+keywords = _ gettext ngettext l_ lazy_gettext
+mapping_file = babel.cfg
+output_file = keystonemiddleware/locale/keystonemiddleware.pot
+
+[wheel]
+universal = 1
diff --git a/keystonemiddleware-moon/setup.py b/keystonemiddleware-moon/setup.py
new file mode 100644 (file)
index 0000000..7363757
--- /dev/null
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
+import setuptools
+
+# In python < 2.7.4, a lazy loading of package `pbr` will break
+# setuptools if some other modules registered functions in `atexit`.
+# solution from: http://bugs.python.org/issue15881#msg170215
+try:
+    import multiprocessing  # noqa
+except ImportError:
+    pass
+
+setuptools.setup(
+    setup_requires=['pbr'],
+    pbr=True)
diff --git a/keystonemiddleware-moon/test-requirements-py3.txt b/keystonemiddleware-moon/test-requirements-py3.txt
new file mode 100644 (file)
index 0000000..ff9e614
--- /dev/null
@@ -0,0 +1,18 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+
+coverage>=3.6
+discover
+fixtures>=0.3.14
+hacking>=0.8.0,<0.9
+mock>=1.0
+pycrypto>=2.6
+oslosphinx>=2.2.0  # Apache-2.0
+oslotest>=1.2.0  # Apache-2.0
+oslo.messaging>=1.6.0  # Apache-2.0
+requests-mock>=0.5.1  # Apache-2.0
+sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
+testrepository>=0.0.18
+testresources>=0.2.4
+testtools>=0.9.36,!=1.2.0
diff --git a/keystonemiddleware-moon/test-requirements.txt b/keystonemiddleware-moon/test-requirements.txt
new file mode 100644 (file)
index 0000000..55d21d5
--- /dev/null
@@ -0,0 +1,20 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+
+hacking>=0.10.0,<0.11
+
+coverage>=3.6
+discover
+fixtures>=0.3.14
+mock>=1.0
+pycrypto>=2.6
+oslosphinx>=2.2.0  # Apache-2.0
+oslotest>=1.2.0  # Apache-2.0
+oslo.messaging>=1.6.0  # Apache-2.0
+requests-mock>=0.5.1  # Apache-2.0
+sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
+testrepository>=0.0.18
+testresources>=0.2.4
+testtools>=0.9.36,!=1.2.0
+python-memcached>=1.48
diff --git a/keystonemiddleware-moon/tools/install_venv_common.py b/keystonemiddleware-moon/tools/install_venv_common.py
new file mode 100644 (file)
index 0000000..e279159
--- /dev/null
@@ -0,0 +1,172 @@
+# Copyright 2013 OpenStack Foundation
+# Copyright 2013 IBM Corp.
+#
+#    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.
+
+"""Provides methods needed by installation script for OpenStack development
+virtual environments.
+
+Since this script is used to bootstrap a virtualenv from the system's Python
+environment, it should be kept strictly compatible with Python 2.6.
+
+Synced in from openstack-common
+"""
+
+from __future__ import print_function
+
+import optparse
+import os
+import subprocess
+import sys
+
+
+class InstallVenv(object):
+
+    def __init__(self, root, venv, requirements,
+                 test_requirements, py_version,
+                 project):
+        self.root = root
+        self.venv = venv
+        self.requirements = requirements
+        self.test_requirements = test_requirements
+        self.py_version = py_version
+        self.project = project
+
+    def die(self, message, *args):
+        print(message % args, file=sys.stderr)
+        sys.exit(1)
+
+    def check_python_version(self):
+        if sys.version_info < (2, 6):
+            self.die("Need Python Version >= 2.6")
+
+    def run_command_with_code(self, cmd, redirect_output=True,
+                              check_exit_code=True):
+        """Runs a command in an out-of-process shell.
+
+        Returns the output of that command. Working directory is self.root.
+        """
+        if redirect_output:
+            stdout = subprocess.PIPE
+        else:
+            stdout = None
+
+        proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
+        output = proc.communicate()[0]
+        if check_exit_code and proc.returncode != 0:
+            self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
+        return (output, proc.returncode)
+
+    def run_command(self, cmd, redirect_output=True, check_exit_code=True):
+        return self.run_command_with_code(cmd, redirect_output,
+                                          check_exit_code)[0]
+
+    def get_distro(self):
+        if (os.path.exists('/etc/fedora-release') or
+                os.path.exists('/etc/redhat-release')):
+            return Fedora(
+                self.root, self.venv, self.requirements,
+                self.test_requirements, self.py_version, self.project)
+        else:
+            return Distro(
+                self.root, self.venv, self.requirements,
+                self.test_requirements, self.py_version, self.project)
+
+    def check_dependencies(self):
+        self.get_distro().install_virtualenv()
+
+    def create_virtualenv(self, no_site_packages=True):
+        """Creates the virtual environment and installs PIP.
+
+        Creates the virtual environment and installs PIP only into the
+        virtual environment.
+        """
+        if not os.path.isdir(self.venv):
+            print('Creating venv...', end=' ')
+            if no_site_packages:
+                self.run_command(['virtualenv', '-q', '--no-site-packages',
+                                 self.venv])
+            else:
+                self.run_command(['virtualenv', '-q', self.venv])
+            print('done.')
+        else:
+            print("venv already exists...")
+            pass
+
+    def pip_install(self, *args):
+        self.run_command(['tools/with_venv.sh',
+                         'pip', 'install', '--upgrade'] + list(args),
+                         redirect_output=False)
+
+    def install_dependencies(self):
+        print('Installing dependencies with pip (this can take a while)...')
+
+        # First things first, make sure our venv has the latest pip and
+        # setuptools and pbr
+        self.pip_install('pip>=1.4')
+        self.pip_install('setuptools')
+        self.pip_install('pbr')
+
+        self.pip_install('-r', self.requirements, '-r', self.test_requirements)
+
+    def parse_args(self, argv):
+        """Parses command-line arguments."""
+        parser = optparse.OptionParser()
+        parser.add_option('-n', '--no-site-packages',
+                          action='store_true',
+                          help="Do not inherit packages from global Python "
+                               "install.")
+        return parser.parse_args(argv[1:])[0]
+
+
+class Distro(InstallVenv):
+
+    def check_cmd(self, cmd):
+        return bool(self.run_command(['which', cmd],
+                    check_exit_code=False).strip())
+
+    def install_virtualenv(self):
+        if self.check_cmd('virtualenv'):
+            return
+
+        if self.check_cmd('easy_install'):
+            print('Installing virtualenv via easy_install...', end=' ')
+            if self.run_command(['easy_install', 'virtualenv']):
+                print('Succeeded')
+                return
+            else:
+                print('Failed')
+
+        self.die('ERROR: virtualenv not found.\n\n%s development'
+                 ' requires virtualenv, please install it using your'
+                 ' favorite package management tool' % self.project)
+
+
+class Fedora(Distro):
+    """This covers all Fedora-based distributions.
+
+    Includes: Fedora, RHEL, CentOS, Scientific Linux
+    """
+
+    def check_pkg(self, pkg):
+        return self.run_command_with_code(['rpm', '-q', pkg],
+                                          check_exit_code=False)[1] == 0
+
+    def install_virtualenv(self):
+        if self.check_cmd('virtualenv'):
+            return
+
+        if not self.check_pkg('python-virtualenv'):
+            self.die("Please install 'python-virtualenv'.")
+
+        super(Fedora, self).install_virtualenv()
diff --git a/keystonemiddleware-moon/tox.ini b/keystonemiddleware-moon/tox.ini
new file mode 100644 (file)
index 0000000..08cd205
--- /dev/null
@@ -0,0 +1,54 @@
+[tox]
+minversion = 1.6
+skipsdist = True
+envlist = py26,py27,py33,py34,pep8
+
+[testenv]
+usedevelop = True
+install_command = pip install -U {opts} {packages}
+setenv = VIRTUAL_ENV={envdir}
+         OS_STDOUT_NOCAPTURE=False
+         OS_STDERR_NOCAPTURE=False
+
+deps = -r{toxinidir}/requirements.txt
+       -r{toxinidir}/test-requirements.txt
+commands = python setup.py testr --testr-args='{posargs}'
+
+[testenv:py33]
+deps = -r{toxinidir}/requirements.txt
+       -r{toxinidir}/test-requirements-py3.txt
+
+[testenv:py34]
+deps = -r{toxinidir}/requirements.txt
+       -r{toxinidir}/test-requirements-py3.txt
+
+[testenv:pep8]
+commands =
+  flake8
+
+[testenv:venv]
+commands = {posargs}
+
+[testenv:cover]
+commands = python setup.py testr --coverage --testr-args='{posargs}'
+
+[tox:jenkins]
+downloadcache = ~/cache/pip
+
+[testenv:debug]
+
+commands = oslo_debug_helper {posargs}
+
+[flake8]
+# H405: multi line docstring summary not separated with an empty line
+ignore = H405
+show-source = True
+exclude = .venv,.tox,dist,doc,*egg,build,*openstack/common*
+
+[testenv:docs]
+commands=
+    python setup.py build_sphinx
+
+[hacking]
+import_exceptions =
+    keystonemiddleware.i18n