pax_global_header00006660000000000000000000000064127124547300014517gustar00rootroot0000000000000052 comment=22d1c9a81ec300503b47530d9042123d315b4d8b keystoneauth-2.4.1/000077500000000000000000000000001271245473000142465ustar00rootroot00000000000000keystoneauth-2.4.1/.coveragerc000066400000000000000000000001471271245473000163710ustar00rootroot00000000000000[run] branch = True source = keystoneauth1 omit = keystoneauth1/tests/* [report] ignore_errors = True keystoneauth-2.4.1/.gitignore000066400000000000000000000005341271245473000162400ustar00rootroot00000000000000.coverage .testrepository subunit.log .venv *,cover cover *.pyc .idea *.sw? *.egg *~ .tox AUTHORS ChangeLog build dist keystoneauth1.egg-info doc/source/api # Development environment files .project .pydevproject # Temporary files created during test, but not removed examples/pki/certs/tmp* # Files created by releasenotes build releasenotes/build keystoneauth-2.4.1/.gitreview000066400000000000000000000001551271245473000162550ustar00rootroot00000000000000[gerrit] host=review.openstack.org port=29418 project=openstack/keystoneauth.git defaultbranch=stable/mitaka keystoneauth-2.4.1/.mailmap000066400000000000000000000003541271245473000156710ustar00rootroot00000000000000# Format is: # # keystoneauth-2.4.1/.testr.conf000066400000000000000000000003021271245473000163270ustar00rootroot00000000000000[DEFAULT] test_command=${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./keystoneauth1/tests/unit} $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list keystoneauth-2.4.1/CONTRIBUTING.rst000066400000000000000000000012211271245473000167030ustar00rootroot00000000000000If you would like to contribute to the development of OpenStack, you must follow the steps documented at: http://docs.openstack.org/infra/manual/developers.html If you already have a good understanding of how the system works and your OpenStack accounts are set up, you can skip to the development workflow section of this documentation to learn how changes to OpenStack should be submitted for review via the Gerrit tool: 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/keystoneauth keystoneauth-2.4.1/HACKING.rst000066400000000000000000000012061271245473000160430ustar00rootroot00000000000000Keystone 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 ======= keystoneauth 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/ keystoneauth-2.4.1/LICENSE000066400000000000000000000271631271245473000152640ustar00rootroot00000000000000Copyright (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 keystoneauth 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. keystoneauth-2.4.1/MANIFEST.in000066400000000000000000000002561271245473000160070ustar00rootroot00000000000000include README.rst include AUTHORS HACKING LICENSE include ChangeLog include run_tests.sh tox.ini recursive-include doc * recursive-include tests * recursive-include tools * keystoneauth-2.4.1/README.rst000066400000000000000000000015601271245473000157370ustar00rootroot00000000000000============ keystoneauth ============ .. image:: https://img.shields.io/pypi/v/keystoneauth1.svg :target: https://pypi.python.org/pypi/keystoneauth1/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/keystoneauth1.svg :target: https://pypi.python.org/pypi/keystoneauth1/ :alt: Downloads This package contains tools for authenticating to an OpenStack-based cloud. These tools include: * Authentication plugins (password, token, and federation based) * Discovery mechanisms to determine API version support * A session that is used to maintain client settings across requests (based on the requests Python library) Further information: * Free software: Apache license * Documentation: http://docs.openstack.org/developer/keystoneauth * Source: http://git.openstack.org/cgit/openstack/keystoneauth * Bugs: http://bugs.launchpad.net/keystoneauth keystoneauth-2.4.1/doc/000077500000000000000000000000001271245473000150135ustar00rootroot00000000000000keystoneauth-2.4.1/doc/.gitignore000066400000000000000000000000071271245473000170000ustar00rootroot00000000000000build/ keystoneauth-2.4.1/doc/Makefile000066400000000000000000000061461271245473000164620ustar00rootroot00000000000000# 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 ' where 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/keystoneauth.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/keystoneauth.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." keystoneauth-2.4.1/doc/source/000077500000000000000000000000001271245473000163135ustar00rootroot00000000000000keystoneauth-2.4.1/doc/source/authentication-plugins.rst000066400000000000000000000243301271245473000235450ustar00rootroot00000000000000====================== Authentication Plugins ====================== Introduction ============ Authentication plugins provide a generic means by which to extend the authentication mechanisms known to OpenStack clients. In the vast majority of cases the authentication plugins used will be those written for use with the OpenStack Identity Service (Keystone), however this is not the only possible case, and the mechanisms by which authentication plugins are used and implemented should be generic enough to cover completely customized authentication solutions. The subset of authentication plugins intended for use with an OpenStack Identity server (such as Keystone) are called Identity Plugins. Available Plugins ================= Keystoneclient ships with a number of plugins and particularly Identity Plugins. V2 Identity Plugins ------------------- Standard V2 identity plugins are defined in the module: :py:mod:`keystoneauth1.identity.v2` They include: - :py:class:`~keystoneauth1.identity.v2.Password`: Authenticate against a V2 identity service using a username and password. - :py:class:`~keystoneauth1.identity.v2.Token`: Authenticate against a V2 identity service using an existing token. V2 identity plugins must use an `auth_url` that points to the root of a V2 identity server URL, i.e.: `http://hostname:5000/v2.0`. V3 Identity Plugins ------------------- Standard V3 identity plugins are defined in the module :py:mod:`keystoneauth1.identity.v3`. V3 Identity plugins are slightly different from their V2 counterparts as a V3 authentication request can contain multiple authentication methods. To handle this V3 defines a number of different :py:class:`~keystoneauth1.identity.v3.AuthMethod` classes: - :py:class:`~keystoneauth1.identity.v3.PasswordMethod`: Authenticate against a V3 identity service using a username and password. - :py:class:`~keystoneauth1.identity.v3.TokenMethod`: Authenticate against a V3 identity service using an existing token. - :py:class:`~keystoneauth1.extras.kerberos.KerberosMethod`: Authenticate against a V3 identity service using Kerberos. The :py:class:`~keystoneauth1.identity.v3.AuthMethod` objects are then passed to the :py:class:`~keystoneauth1.identity.v3.Auth` plugin:: >>> from keystoneauth1 import session >>> from keystoneauth1.identity import v3 >>> password = v3.PasswordMethod(username='user', ... password='password', ... user_domain_name='default') >>> auth = v3.Auth(auth_url='http://my.keystone.com:5000/v3', ... auth_methods=[password], ... project_id='projectid') >>> sess = session.Session(auth=auth) As in the majority of cases you will only want to use one :py:class:`~keystoneauth1.identity.v3.AuthMethod` there are also helper authentication plugins for the various :py:class:`~keystoneauth1.identity.v3.AuthMethod` which can be used more like the V2 plugins: - :py:class:`~keystoneauth1.identity.v3.Password`: Authenticate using only a :py:class:`~keystoneauth1.identity.v3.PasswordMethod`. - :py:class:`~keystoneauth1.identity.v3.Token`: Authenticate using only a :py:class:`~keystoneauth1.identity.v3.TokenMethod`. - :py:class:`~keystoneauth1.extras.kerberos.Kerberos`: Authenticate using only a :py:class:`~keystoneauth1.extras.kerberos.KerberosMethod`. :: >>> auth = v3.Password(auth_url='http://my.keystone.com:5000/v3', ... username='username', ... password='password', ... project_id='projectid', ... user_domain_name='default') >>> sess = session.Session(auth=auth) This will have exactly the same effect as using the single :py:class:`~keystoneauth1.identity.v3.PasswordMethod` above. V3 identity plugins must use an `auth_url` that points to the root of a V3 identity server URL, i.e.: `http://hostname:5000/v3`. Version Independent Identity Plugins ------------------------------------ Standard version independent identity plugins are defined in the module :py:mod:`keystoneauth1.identity.generic`. For the cases of plugins that exist under both the identity V2 and V3 APIs there is an abstraction to allow the plugin to determine which of the V2 and V3 APIs are supported by the server and use the most appropriate API. These plugins are: - :py:class:`~keystoneauth1.identity.generic.Password`: Authenticate using a user/password against either v2 or v3 API. - :py:class:`~keystoneauth1.identity.generic.Token`: Authenticate using an existing token against either v2 or v3 API. These plugins work by first querying the identity server to determine available versions and so the `auth_url` used with the plugins should point to the base URL of the identity server to use. If the `auth_url` points to either a V2 or V3 endpoint it will restrict the plugin to only working with that version of the API. Simple Plugins -------------- In addition to the Identity plugins a simple plugin that will always use the same provided token and endpoint is available. This is useful in situations where you have an ``ADMIN_TOKEN`` or in testing when you specifically know the endpoint you want to communicate with. It can be found at :py:class:`keystoneauth1.token_endpoint.Token`. V3 OAuth 1.0a Plugins --------------------- There also exists a plugin for OAuth 1.0a authentication. We provide a helper authentication plugin at: :py:class:`~keystoneauth1.v3.contrib.oauth1.auth.OAuth`. The plugin requires the OAuth consumer's key and secret, as well as the OAuth access token's key and secret. For example:: >>> from keystoneauth1.v3.contrib.oauth1 import auth >>> from keystoneauth1 import session >>> from keystoneauth1.v3 import client >>> a = auth.OAuth('http://my.keystone.com:5000/v3', ... consumer_key=consumer_id, ... consumer_secret=consumer_secret, ... access_key=access_token_key, ... access_secret=access_token_secret) >>> s = session.Session(auth=a) Loading Plugins by Name ======================= In auth_token middleware and for some service to service communication it is possible to specify a plugin to load via name. The authentication options that are available are then specific to the plugin that you specified. Currently the authentication plugins that are available in `keystoneauth` are: - password: :py:class:`keystoneauth1.identity.generic.Password` - token: :py:class:`keystoneauth1.identity.generic.Token` - v2password: :py:class:`keystoneauth1.identity.v2.Password` - v2token: :py:class:`keystoneauth1.identity.v2.Token` - v3password: :py:class:`keystoneauth1.identity.v3.Password` - v3token: :py:class:`keystoneauth1.identity.v3.Token` Creating Authentication Plugins =============================== Creating an Identity Plugin --------------------------- If you have implemented a new authentication mechanism into the Identity service then you will be able to reuse a lot of the infrastructure available for the existing Identity mechanisms. As the V2 identity API is essentially frozen, it is expected that new plugins are for the V3 API. To implement a new V3 plugin that can be combined with others you should implement the base :py:class:`keystoneauth1.identity.v3.AuthMethod` class and implement the :py:meth:`~keystoneauth1.identity.v3.AuthMethod.get_auth_data` function. If your Plugin cannot be used in conjunction with existing :py:class:`keystoneauth1.identity.v3.AuthMethod` then you should just override :py:class:`keystoneauth1.identity.v3.Auth` directly. The new :py:class:`~keystoneauth1.identity.v3.AuthMethod` should take all the required parameters via :py:meth:`~keystoneauth1.identity.v3.AuthMethod.__init__` and return from :py:meth:`~keystoneauth1.identity.v3.AuthMethod.get_auth_data` a tuple with the unique identifier of this plugin (e.g. *password*) and a dictionary containing the payload of values to send to the authentication server. The session, calling auth object and request headers are also passed to this function so that the plugin may use or manipulate them. You should also provide a class that inherits from :py:class:`keystoneauth1.identity.v3.Auth` with an instance of your new :py:class:`~keystoneauth1.identity.v3.AuthMethod` as the `auth_methods` parameter to :py:class:`keystoneauth1.identity.v3.Auth`. By convention (and like above) these are named `PluginType` and `PluginTypeMethod` (for example :py:class:`~keystoneauth1.identity.v3.Password` and :py:class:`~keystoneauth1.identity.v3.PasswordMethod`). Creating a Custom Plugin ------------------------ To implement an entirely new plugin you should implement the base class :py:class:`keystoneauth1.plugin.BaseAuthPlugin` and provide the :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.get_endpoint`, :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.get_token` and :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.invalidate` methods. :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.get_token` is called to retrieve the string token from a plugin. It is intended that a plugin will cache a received token and so if the token is still valid then it should be re-used rather than fetching a new one. A session object is provided with which the plugin can contact it's server. (Note: use `authenticated=False` when making those requests or it will end up being called recursively). The return value should be the token as a string. :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.get_endpoint` is called to determine a base URL for a particular service's requests. The keyword arguments provided to the function are those that are given by the `endpoint_filter` variable in :py:meth:`keystoneauth1.session.Session.request`. A session object is also provided so that the plugin may contact an external source to determine the endpoint. Again this will be generally be called once per request and so it is up to the plugin to cache these responses if appropriate. The return value should be the base URL to communicate with. :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.invalidate` should also be implemented to clear the current user credentials so that on the next :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.get_token` call a new token can be retrieved. The most simple example of a plugin is the :py:class:`keystoneauth1.token_endpoint.Token` plugin. keystoneauth-2.4.1/doc/source/conf.py000066400000000000000000000164441271245473000176230ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # keystoneauth1 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 subprocess import sys import pbr.version 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', '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 = 'keystoneauth1' 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('keystoneauth1') # 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 = ['keystoneauth1.'] # 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 # " v 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 = subprocess.Popen( git_cmd, stdout=subprocess.PIPE).communicate()[0] # 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 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 = 'keystoneauthdoc' # -- 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', 'keystoneauth1.tex', 'keystoneauth1 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 ksc = 'http://docs.openstack.org/developer/python-keystoneclient/' intersphinx_mapping = { 'python': ('http://docs.python.org/', None), 'osloconfig': ('http://docs.openstack.org/developer/oslo.config/', None), 'keystoneclient': (ksc, None), } keystoneauth-2.4.1/doc/source/extras.rst000066400000000000000000000021541271245473000203550ustar00rootroot00000000000000====== Extras ====== The extensibility of keystoneauth plugins is purposefully designed to allow a range of different authentication mechanisms that don't have to reside in the upstream packages. There are however a number of plugins that upstream supports that involve additional dependencies that the keystoneauth package cannot depend upon directly. To get around this we utilize setuptools `extras dependencies `_ for additional plugins. To use a plugin like the kerberos plugin that has additional dependencies you must install the additional dependencies like:: pip install keystoneauth1[kerberos] By convention (not a requirement) extra plugins have a module located in the keystoneauth1.extras module with the same name as the dependency. eg:: $ from keystoneauth1.extras import kerberos There is no keystoneauth specific check that the correct dependencies are installed for accessing a module. You would expect to see standard python ImportError when the required dependencies are not found. keystoneauth-2.4.1/doc/source/history.rst000066400000000000000000000000351271245473000205440ustar00rootroot00000000000000.. include:: ../../ChangeLog keystoneauth-2.4.1/doc/source/images/000077500000000000000000000000001271245473000175605ustar00rootroot00000000000000keystoneauth-2.4.1/doc/source/images/graphs_authComp.svg000066400000000000000000000057061271245473000234350ustar00rootroot00000000000000 AuthComp AuthComp Auth Component AuthComp->Reject Reject Unauthenticated Requests Service OpenStack Service AuthComp->Service Forward Authenticated Requests Start->AuthComp keystoneauth-2.4.1/doc/source/images/graphs_authCompDelegate.svg000066400000000000000000000070201271245473000250570ustar00rootroot00000000000000 AuthCompDelegate AuthComp Auth Component AuthComp->Reject Reject Requests Indicated by the Service Service OpenStack Service AuthComp->Service Forward Requests with Identiy Status Service->AuthComp Send Response OR Reject Message Start->AuthComp keystoneauth-2.4.1/doc/source/index.rst000066400000000000000000000020371271245473000201560ustar00rootroot00000000000000Common Authentication Library for OpenStack Clients =================================================== Keystoneauth provides a standard way to do authentication and service requests within the OpenStack ecosystem. It is designed for use in conjunction with the existing OpenStack clients and for simplifying the process of writing new clients. Contents: .. toctree:: :maxdepth: 1 using-sessions authentication-plugins extras migrating api/modules Release Notes ============= .. toctree:: :maxdepth: 1 history Contributing ============ Code is hosted `on GitHub`_. Submit bugs to the Keystone project on `Launchpad`_. Submit code to the ``openstack/keystoneauth`` project using `Gerrit`_. .. _on GitHub: https://github.com/openstack/keystoneauth .. _Launchpad: https://launchpad.net/keystoneauth .. _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` keystoneauth-2.4.1/doc/source/migrating.rst000066400000000000000000000045161271245473000210340ustar00rootroot00000000000000============================= Migrating from keystoneclient ============================= When keystoneauth was extracted from keystoneclient the basic usage of the session, adapter and auth plugins purposefully did not change. If you are using them in a supported fashion from keystoneclient then the transition should be fairly simple. Authentication Plugins ====================== The authentication plugins themselves changed very little however there were changes to the way plugins are loaded and some of the supporting classes. Plugin Loading -------------- In keystoneclient auth plugin loading is managed by the class itself. This method proved useful in allowing the plugin to control the way it was loaded however it linked the authentication logic with the config and CLI loading. In keystoneauth this has been severed and the auth plugin is handle seperately from the mechanism that loads it. Authentication plugins still implement the base authentication class :py:class:`~keystoneauth1.plugin.BaseAuthPlugin`. To make the plugins capable of being loaded from CLI or CONF file you implement a :py:class:`~keystoneauth1.loading.BaseLoader` object that is loaded when a user does '--os-auth-type', handles the options that are presented, and then constructs the authentication plugin for use by the application. Largely the options that are returned will be the same as what was used in keystoneclient however in keystoneclient the options used :py:class:`oslo_config.cfg.Opt` objects. Due to trying to keep minimal dependencies there is no direct dependency from keystoneauth on oslo.config and instead options should be specified as :py:class:`~keystoneauth1.loading.Opt` objects. To ensure distinction between the plugins the setuptools entypoints that plugins register at has been updated to reflect keystoneauth1 and should now be: keystoneauth1.plugin AccessInfo Objects ------------------ AccessInfo objects are a representation of the information stored within a token. In keystoneclient these objects were dictionaries of the token data with property accessors. In keystoneauth the dictionary interface has been removed and just the property accessors are available. The creation function has also changed. The :py:meth:`keystoneclient.access.AccessInfo.factory` method has been removed and replaced with the :py:func:`keystoneauth1.access.create`. keystoneauth-2.4.1/doc/source/using-sessions.rst000066400000000000000000000161201271245473000220360ustar00rootroot00000000000000============== Using Sessions ============== Introduction ============ The :py:class:`keystoneauth1.session.Session` class was introduced into keystoneauth1 as an attempt to bring a unified interface to the various OpenStack clients that share common authentication and request parameters between a variety of services. The model for using a Session and auth plugin as well as the general terms used have been heavily inspired by the `requests `_ library. However neither the Session class nor any of the authentication plugins rely directly on those concepts from the requests library so you should not expect a direct translation. Features -------- - Common client authentication Authentication is handled by one of a variety of authentication plugins and then this authentication information is shared between all the services that use the same Session object. - Security maintenance Security code is maintained in a single place and reused between all clients such that in the event of problems it can be fixed in a single location. - Standard discovery mechanisms Clients are not expected to have any knowledge of an identity token or any other form of identification credential. Service and endpoint discovery are handled by the Session and plugins. Sessions for Users ================== The Session object is the contact point to your OpenStack cloud services. It stores the authentication credentials and connection information required to communicate with OpenStack such that it can be reused to communicate with many services. When creating services this Session object is passed to the client so that it may use this information. A Session will authenticate on demand. When a request that requires authentication passes through the Session the authentication plugin will be asked for a valid token. If a valid token is available it will be used otherwise the authentication plugin may attempt to contact the authentication service and fetch a new one. An example from keystoneclient:: >>> from keystoneauth1.identity import v3 >>> from keystoneauth1 import session >>> from keystoneclient.v3 import client >>> auth = v3.Password(auth_url='https://my.keystone.com:5000/v3', ... username='myuser', ... password='mypassword', ... project_name='proj', ... user_domain_id='default', ... project_domain_id='default') >>> sess = session.Session(auth=auth, ... verify='/path/to/ca.cert') >>> ks = client.Client(session=sess) >>> users = ks.users.list() As clients adopt this means of operating they will be created in a similar fashion by passing the Session object to the client's constructor. Sharing Authentication Plugins ------------------------------ A session can only contain one authentication plugin however there is nothing that specifically binds the authentication plugin to that session, a new Session can be created that reuses the existing authentication plugin:: >>> new_sess = session.Session(auth=sess.auth, verify='/path/to/different-cas.cert') In this case we cannot know which session object will be used when the plugin performs the authentication call so the command must be able to succeed with either. Authentication plugins can also be provided on a per-request basis. This will be beneficial in a situation where a single session is juggling multiple authentication credentials:: >>> sess.get('https://my.keystone.com:5000/v3', auth=my_auth_plugin) If an auth plugin is provided via parameter then it will override any auth plugin on the session. Sessions for Client Developers ============================== Sessions are intended to take away much of the hassle of dealing with authentication data and token formats. Clients should be able to specify filter parameters for selecting the endpoint and have the parsing of the catalog managed for them. Authentication -------------- When making a request with a session object you can simply pass the keyword parameter ``authenticated`` to indicate whether the argument should contain a token, by default a token is included if an authentication plugin is available:: >>> # In keystone this route is unprotected by default >>> resp = sess.get('https://my.keystone.com:5000/v3', authenticated=False) Service Discovery ----------------- In OpenStack the URLs of available services are distributed to the user as a part of the token they receive called the Service Catalog. Clients are expected to use the URLs from the Service Catalog rather than have them provided. In general a client does not need to know the full URL for the server that they are communicating with, simply that it should send a request to a path belonging to the correct service. This is controlled by the ``endpoint_filter`` parameter to a request which contains all the information an authentication plugin requires to determine the correct URL to which to send a request. When using this mode only the path for the request needs to be specified:: >>> resp = session.get('/v3/users', endpoint_filter={'service_type': 'identity', 'interface': 'public', 'region_name': 'myregion'}) ``endpoint_filter`` accepts a number of arguments with which it can determine an endpoint url: - ``service_type``: the type of service. For example ``identity``, ``compute``, ``volume`` or many other predefined identifiers. - ``interface``: the network exposure the interface has. This will be one of: - ``public``: An endpoint that is available to the wider internet or network. - ``internal``: An endpoint that is only accessible within the private network. - ``admin``: An endpoint to be used for administrative tasks. - ``region_name``: the name of the region where the endpoint resides. The endpoint filter is a simple key-value filter and can be provided with any number of arguments. It is then up to the auth plugin to correctly use the parameters it understands. The session object determines the URL matching the filter and append to it the provided path and so create a valid request. If multiple URL matches are found then any one may be chosen. While authentication plugins will endeavour to maintain a consistent set of arguments for an ``endpoint_filter`` the concept of an authentication plugin is purposefully generic and a specific mechanism may not know how to interpret certain arguments and ignore them. For example the :class:`keystoneauth1.token_endpoint.Token` plugin (which is used when you want to always use a specific endpoint and token combination) will always return the same endpoint regardless of the parameters to ``endpoint_filter`` or a custom OpenStack authentication mechanism may not have the concept of multiple ``interface`` options and choose to ignore that parameter. There is some expectation on the user that they understand the limitations of the authentication system they are using. keystoneauth-2.4.1/keystoneauth1/000077500000000000000000000000001271245473000170525ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/__init__.py000066400000000000000000000011771271245473000211710ustar00rootroot00000000000000# 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 pbr.version __version__ = pbr.version.VersionInfo('keystoneauth1').version_string() keystoneauth-2.4.1/keystoneauth1/_utils.py000066400000000000000000000042461271245473000207310ustar00rootroot00000000000000# 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 iso8601 import six def get_logger(name): name = name.replace(__name__.split('.')[0], 'keystoneauth') return logging.getLogger(name) logger = get_logger(__name__) def normalize_time(timestamp): """Normalize time in arbitrary timezone to UTC naive object.""" offset = timestamp.utcoffset() if offset is None: return timestamp return timestamp.replace(tzinfo=None) - offset def parse_isotime(timestr): """Parse time from ISO 8601 format.""" try: return iso8601.parse_date(timestr) except iso8601.ParseError as e: raise ValueError(six.text_type(e)) except TypeError as e: raise ValueError(six.text_type(e)) def from_utcnow(**timedelta_kwargs): """Calculate the time in the future from utcnow. :param \*\*timedelta_kwargs: Passed directly to :class:`datetime.timedelta` to add to the current time in UTC. :returns: The time in the future based on ``timedelta_kwargs``. :rtype: datetime.datetime """ now = datetime.datetime.utcnow() delta = datetime.timedelta(**timedelta_kwargs) return now + delta def before_utcnow(**timedelta_kwargs): """Calculate the time in the past from utcnow. :param \*\*timedelta_kwargs: Passed directly to :class:`datetime.timedelta` to subtract from the current time in UTC. :returns: The time in the past based on ``timedelta_kwargs``. :rtype: datetime.datetime """ now = datetime.datetime.utcnow() delta = datetime.timedelta(**timedelta_kwargs) return now - delta keystoneauth-2.4.1/keystoneauth1/access/000077500000000000000000000000001271245473000203135ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/access/__init__.py000066400000000000000000000012721271245473000224260ustar00rootroot00000000000000# 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 keystoneauth1.access.access import * # noqa __all__ = ('AccessInfo', 'AccessInfoV2', 'AccessInfoV3', 'create') keystoneauth-2.4.1/keystoneauth1/access/access.py000066400000000000000000000433711271245473000221360ustar00rootroot00000000000000# Copyright 2012 Nebula, 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. import functools from positional import positional from keystoneauth1 import _utils as utils from keystoneauth1.access import service_catalog from keystoneauth1.access import service_providers # gap, in seconds, to determine whether the given token is about to expire STALE_TOKEN_DURATION = 30 __all__ = ('AccessInfo', 'AccessInfoV2', 'AccessInfoV3', 'create') @positional() def create(resp=None, body=None, auth_token=None): if resp and not body: body = resp.json() if 'token' in body: if resp and not auth_token: auth_token = resp.headers.get('X-Subject-Token') return AccessInfoV3(body, auth_token) elif 'access' in body: return AccessInfoV2(body, auth_token) raise ValueError('Unrecognized auth response') def _missingproperty(f): @functools.wraps(f) def inner(self): try: return f(self) except KeyError: return None return property(inner) class AccessInfo(object): """Encapsulates a raw authentication token from keystone. Provides helper methods for extracting useful values from that token. """ _service_catalog_class = None def __init__(self, body, auth_token=None): self._data = body self._auth_token = auth_token self._service_catalog = None self._service_providers = None @property def service_catalog(self): if not self._service_catalog: self._service_catalog = self._service_catalog_class.from_token( self._data) return self._service_catalog def will_expire_soon(self, stale_duration=STALE_TOKEN_DURATION): """Determines if expiration is about to occur. :returns: true if expiration is within the given duration :rtype: boolean """ norm_expires = utils.normalize_time(self.expires) # (gyee) should we move auth_token.will_expire_soon() to timeutils # instead of duplicating code here? soon = utils.from_utcnow(seconds=stale_duration) return norm_expires < soon def has_service_catalog(self): """Returns true if the auth token has a service catalog. :returns: boolean """ raise NotImplementedError() @property def auth_token(self): """Returns the token_id associated with the auth request. To be used in headers for authenticating OpenStack API requests. :returns: str """ return self._auth_token @property def expires(self): """Returns the token expiration (as datetime object) :returns: datetime """ raise NotImplementedError() @property def issued(self): """Returns the token issue time (as datetime object) :returns: datetime """ raise NotImplementedError() @property def username(self): """Returns the username associated with the auth request. Follows the pattern defined in the V2 API of first looking for 'name', returning that if available, and falling back to 'username' if name is unavailable. :returns: str """ raise NotImplementedError() @property def user_id(self): """Returns the user id associated with the auth request. :returns: str """ raise NotImplementedError() @property def user_domain_id(self): """Returns the user's domain id associated with the auth request. :returns: str """ raise NotImplementedError() @property def user_domain_name(self): """Returns the user's domain name associated with the auth request. :returns: str """ raise NotImplementedError() @property def role_ids(self): """Returns a list of user's role ids associated with the auth request. :returns: a list of strings of role ids """ raise NotImplementedError() @property def role_names(self): """Returns a list of user's role names associated with the auth request. :returns: a list of strings of role names """ raise NotImplementedError() @property def domain_name(self): """Returns the domain name associated with the auth request. :returns: str or None (if no domain associated with the token) """ raise NotImplementedError() @property def domain_id(self): """Returns the domain id associated with the auth request. :returns: str or None (if no domain associated with the token) """ raise NotImplementedError() @property def project_name(self): """Returns the project name associated with the auth request. :returns: str or None (if no project associated with the token) """ raise NotImplementedError() @property def tenant_name(self): """Synonym for project_name.""" return self.project_name @property def scoped(self): """Returns true if the auth token was scoped. Returns true if scoped to a tenant(project) or domain, and contains a populated service catalog. This is deprecated, use project_scoped instead. :returns: bool """ return self.project_scoped or self.domain_scoped @property def project_scoped(self): """Returns true if the auth token was scoped to a tenant(project). :returns: bool """ return bool(self.project_id) @property def domain_scoped(self): """Returns true if the auth token was scoped to a domain. :returns: bool """ raise NotImplementedError() @property def trust_id(self): """Returns the trust id associated with the auth request. :returns: str or None (if no trust associated with the token) """ raise NotImplementedError() @property def trust_scoped(self): """Returns true if the auth token was scoped from a delegated trust. The trust delegation is via the OS-TRUST v3 extension. :returns: bool """ raise NotImplementedError() @property def trustee_user_id(self): """Returns the trustee user id associated with a trust. :returns: str or None (if no trust associated with the token) """ raise NotImplementedError() @property def trustor_user_id(self): """Returns the trustor user id associated with a trust. :returns: str or None (if no trust associated with the token) """ raise NotImplementedError() @property def project_id(self): """Returns the project ID associated with the auth request. This returns None if the auth token wasn't scoped to a project. :returns: str or None (if no project associated with the token) """ raise NotImplementedError() @property def tenant_id(self): """Synonym for project_id.""" return self.project_id @property def project_domain_id(self): """Returns the project's domain id associated with the auth request. :returns: str """ raise NotImplementedError() @property def project_domain_name(self): """Returns the project's domain name associated with the auth request. :returns: str """ raise NotImplementedError() @property def oauth_access_token_id(self): """Return the access token ID if OAuth authentication used. :returns: str or None. """ raise NotImplementedError() @property def oauth_consumer_id(self): """Return the consumer ID if OAuth authentication used. :returns: str or None. """ raise NotImplementedError() @property def is_federated(self): """Returns true if federation was used to get the token. :returns: boolean """ raise NotImplementedError() @property def audit_id(self): """Return the audit ID if present. :returns: str or None. """ raise NotImplementedError() @property def audit_chain_id(self): """Return the audit chain ID if present. In the event that a token was rescoped then this ID will be the :py:attr:`audit_id` of the initial token. Returns None if no value present. :returns: str or None. """ raise NotImplementedError() @property def initial_audit_id(self): """The audit ID of the initially requested token. This is the :py:attr:`audit_chain_id` if present or the :py:attr:`audit_id`. """ return self.audit_chain_id or self.audit_id @property def service_providers(self): """Return an object representing the list of trusted service providers. Used for Keystone2Keystone federating-out. :returns: :py:class:`keystoneauth1.service_providers.ServiceProviders` or None """ raise NotImplementedError() @property def bind(self): """Information about external mechanisms the token is bound to. If a token is bound to an external authentication mechanism it can only be used in conjunction with that mechanism. For example if bound to a kerberos principal it may only be accepted if there is also kerberos authentication performed on the request. :returns: A dictionary or None. The key will be the bind type the value is a dictionary that is specific to the format of the bind type. Returns None if there is no bind information in the token. """ raise NotImplementedError() class AccessInfoV2(AccessInfo): """An object for encapsulating raw v2 auth token from identity service.""" version = 'v2.0' _service_catalog_class = service_catalog.ServiceCatalogV2 def has_service_catalog(self): return 'serviceCatalog' in self._data.get('access', {}) @_missingproperty def auth_token(self): set_token = super(AccessInfoV2, self).auth_token return set_token or self._data['access']['token']['id'] @property def _token(self): return self._data['access']['token'] @_missingproperty def expires(self): return utils.parse_isotime(self._token.get('expires')) @_missingproperty def issued(self): return utils.parse_isotime(self._token['issued_at']) @property def _user(self): return self._data['access']['user'] @_missingproperty def username(self): return self._user.get('name') or self._user.get('username') @_missingproperty def user_id(self): return self._user['id'] @property def user_domain_id(self): return None @property def user_domain_name(self): return None @_missingproperty def role_ids(self): metadata = self._data.get('access', {}).get('metadata', {}) return metadata.get('roles', []) @_missingproperty def role_names(self): return [r['name'] for r in self._user.get('roles', [])] @property def domain_name(self): return None @property def domain_id(self): return None @property def project_name(self): try: tenant_dict = self._token['tenant'] except KeyError: pass else: return tenant_dict.get('name') # pre grizzly try: return self._user['tenantName'] except KeyError: pass # pre diablo, keystone only provided a tenantId try: return self._token['tenantId'] except KeyError: pass @property def domain_scoped(self): return False @property def _trust(self): return self._data['access']['trust'] @_missingproperty def trust_id(self): return self._trust['id'] @_missingproperty def trust_scoped(self): return bool(self._trust) @_missingproperty def trustee_user_id(self): return self._trust['trustee_user_id'] @property def trustor_user_id(self): # this information is not available in the v2 token bug: #1331882 return None @property def project_id(self): try: tenant_dict = self._token['tenant'] except KeyError: pass else: return tenant_dict.get('id') # pre grizzly try: return self._user['tenantId'] except KeyError: pass # pre diablo try: return self._token['tenantId'] except KeyError: pass @property def project_domain_id(self): return None @property def project_domain_name(self): return None @property def oauth_access_token_id(self): return None @property def oauth_consumer_id(self): return None @property def is_federated(self): return False @property def audit_id(self): try: return self._token.get('audit_ids', [])[0] except IndexError: return None @property def audit_chain_id(self): try: return self._token.get('audit_ids', [])[1] except IndexError: return None @property def service_providers(self): return None @_missingproperty def bind(self): return self._token['bind'] class AccessInfoV3(AccessInfo): """An object encapsulating raw v3 auth token from identity service.""" version = 'v3' _service_catalog_class = service_catalog.ServiceCatalogV3 def has_service_catalog(self): return 'catalog' in self._data['token'] @property def _user(self): return self._data['token']['user'] @property def is_federated(self): return 'OS-FEDERATION' in self._user @_missingproperty def expires(self): return utils.parse_isotime(self._data['token']['expires_at']) @_missingproperty def issued(self): return utils.parse_isotime(self._data['token']['issued_at']) @_missingproperty def user_id(self): return self._user['id'] @property def user_domain_id(self): try: return self._user['domain']['id'] except KeyError: if self.is_federated: return None raise @property def user_domain_name(self): try: return self._user['domain']['name'] except KeyError: if self.is_federated: return None raise @_missingproperty def role_ids(self): return [r['id'] for r in self._data['token'].get('roles', [])] @_missingproperty def role_names(self): return [r['name'] for r in self._data['token'].get('roles', [])] @_missingproperty def username(self): return self._user['name'] @property def _domain(self): return self._data['token']['domain'] @_missingproperty def domain_name(self): return self._domain['name'] @_missingproperty def domain_id(self): return self._domain['id'] @property def _project(self): return self._data['token']['project'] @_missingproperty def project_id(self): return self._project['id'] @_missingproperty def project_domain_id(self): return self._project['domain']['id'] @_missingproperty def project_domain_name(self): return self._project['domain']['name'] @_missingproperty def project_name(self): return self._project['name'] @property def domain_scoped(self): try: return bool(self._domain) except KeyError: return False @property def _trust(self): return self._data['token']['OS-TRUST:trust'] @_missingproperty def trust_id(self): return self._trust['id'] @property def trust_scoped(self): try: return bool(self._trust) except KeyError: return False @_missingproperty def trustee_user_id(self): return self._trust['trustee_user']['id'] @_missingproperty def trustor_user_id(self): return self._trust['trustor_user']['id'] @property def _oauth(self): return self._data['token']['OS-OAUTH1'] @_missingproperty def oauth_access_token_id(self): return self._oauth['access_token_id'] @_missingproperty def oauth_consumer_id(self): return self._oauth['consumer_id'] @_missingproperty def audit_id(self): try: return self._data['token']['audit_ids'][0] except IndexError: return None @_missingproperty def audit_chain_id(self): try: return self._data['token']['audit_ids'][1] except IndexError: return None @property def service_providers(self): if not self._service_providers: self._service_providers = ( service_providers.ServiceProviders.from_token(self._data)) return self._service_providers @_missingproperty def bind(self): return self._data['token']['bind'] keystoneauth-2.4.1/keystoneauth1/access/service_catalog.py000066400000000000000000000270611271245473000240250ustar00rootroot00000000000000# Copyright 2011 OpenStack Foundation # Copyright 2011, Piston Cloud Computing, Inc. # Copyright 2011 Nebula, 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. import abc from positional import positional import six from keystoneauth1 import exceptions @six.add_metaclass(abc.ABCMeta) class ServiceCatalog(object): """Helper methods for dealing with a Keystone Service Catalog.""" def __init__(self, catalog): self._catalog = catalog def _get_endpoint_region(self, endpoint): return endpoint.get('region_id') or endpoint.get('region') @property def catalog(self): """Return the raw service catalog content, mostly useful for debugging. Applications should avoid this and use accessor methods instead. However, there are times when inspecting the raw catalog can be useful for analysis and other reasons. """ return self._catalog @abc.abstractmethod def is_interface_match(self, endpoint, interface): """Helper function to normalize endpoint matching across v2 and v3. :returns: True if the provided endpoint matches the required interface otherwise False. """ @staticmethod def normalize_interface(self, interface): """Handle differences in the way v2 and v3 catalogs specify endpoint. Both v2 and v3 must be able to handle the endpoint style of the other. For example v2 must be able to handle a 'public' interface and v3 must be able to handle a 'publicURL' interface. :returns: the endpoint string in the format appropriate for this service catalog. """ return interface @positional() def get_endpoints(self, service_type=None, interface=None, region_name=None, service_name=None, service_id=None, endpoint_id=None): """Fetch and filter endpoints for the specified service(s). Returns endpoints for the specified service (or all) containing the specified type (or all) and region (or all) and service name. If there is no name in the service catalog the service_name check will be skipped. This allows compatibility with services that existed before the name was available in the catalog. """ interface = self.normalize_interface(interface) sc = {} for service in (self._catalog or []): try: st = service['type'] except KeyError: continue if service_type and service_type != st: continue # NOTE(jamielennox): service_name is different. It is not available # in API < v3.3. If it is in the catalog then we enforce it, if it # is not then we don't because the name could be correct we just # don't have that information to check against. if service_name: try: sn = service['name'] except KeyError: # assume that we're in v3.0-v3.2 and don't have the name in # the catalog. Skip the check. pass else: if service_name != sn: continue # NOTE(jamielennox): there is no such thing as a service_id in v2 # similarly to service_name we'll have to skip this check if it's # not available. if service_id and 'id' in service and service_id != service['id']: continue endpoints = sc.setdefault(st, []) for endpoint in service.get('endpoints', []): if (interface and not self.is_interface_match(endpoint, interface)): continue if (region_name and region_name != self._get_endpoint_region(endpoint)): continue if (endpoint_id and endpoint_id != endpoint.get('id')): continue endpoints.append(endpoint) return sc def _get_service_endpoints(self, service_type=None, **kwargs): sc_endpoints = self.get_endpoints(service_type=service_type, **kwargs) if service_type: endpoints = sc_endpoints.get(service_type, []) else: # flatten list of lists endpoints = [x for endpoint in six.itervalues(sc_endpoints) for x in endpoint] return endpoints @abc.abstractmethod @positional() def get_urls(self, service_type=None, interface='public', region_name=None, service_name=None, service_id=None, endpoint_id=None): """Fetch endpoint urls from the service catalog. Fetch the endpoints from the service catalog for a particular endpoint attribute. If no attribute is given, return the first endpoint of the specified type. :param string service_type: Service type of the endpoint. :param string interface: Type of endpoint. Possible values: public or publicURL, internal or internalURL, admin or adminURL :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. :param string endpoint_id: The identifier of an endpoint. :returns: tuple of urls or None (if no match found) """ raise NotImplementedError() @positional() def url_for(self, service_type=None, interface='public', region_name=None, service_name=None, service_id=None, endpoint_id=None): """Fetch an endpoint from the service catalog. Fetch the specified endpoint from the service catalog for a particular endpoint attribute. If no attribute is given, return the first endpoint of the specified type. Valid endpoint types: `public` or `publicURL`, `internal` or `internalURL`, `admin` or 'adminURL` :param string service_type: Service type of the endpoint. :param string interface: Type of endpoint. :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. :param string endpoint_id: The identifier of an endpoint. """ if not self._catalog: raise exceptions.EmptyCatalog('The service catalog is empty.') urls = self.get_urls(service_type=service_type, interface=interface, region_name=region_name, service_name=service_name, service_id=service_id, endpoint_id=endpoint_id) try: return urls[0] except Exception: pass if service_name and region_name: msg = ('%(interface)s endpoint for %(service_type)s service ' 'named %(service_name)s in %(region_name)s region not ' 'found' % {'interface': interface, 'service_type': service_type, 'service_name': service_name, 'region_name': region_name}) elif service_name: msg = ('%(interface)s endpoint for %(service_type)s service ' 'named %(service_name)s not found' % {'interface': interface, 'service_type': service_type, 'service_name': service_name}) elif region_name: msg = ('%(interface)s endpoint for %(service_type)s service ' 'in %(region_name)s region not found' % {'interface': interface, 'service_type': service_type, 'region_name': region_name}) else: msg = ('%(interface)s endpoint for %(service_type)s service ' 'not found' % {'interface': interface, 'service_type': service_type}) raise exceptions.EndpointNotFound(msg) class ServiceCatalogV2(ServiceCatalog): """An object for encapsulating the v2 service catalog. The object is created using raw v2 auth token from Keystone. """ @classmethod def from_token(cls, token): if 'access' not in token: raise ValueError('Invalid token format for fetching catalog') return cls(token['access'].get('serviceCatalog', {})) @staticmethod def normalize_interface(interface): if interface and 'URL' not in interface: interface = interface + 'URL' return interface def is_interface_match(self, endpoint, interface): return interface in endpoint @positional() def get_urls(self, service_type=None, interface='publicURL', region_name=None, service_name=None, service_id=None, endpoint_id=None): interface = self.normalize_interface(interface) endpoints = self._get_service_endpoints(service_type=service_type, interface=interface, region_name=region_name, service_name=service_name, service_id=service_id, endpoint_id=endpoint_id) return tuple([endpoint[interface] for endpoint in endpoints]) class ServiceCatalogV3(ServiceCatalog): """An object for encapsulating the v3 service catalog. The object is created using raw v3 auth token from Keystone. """ @classmethod def from_token(cls, token): if 'token' not in token: raise ValueError('Invalid token format for fetching catalog') return cls(token['token'].get('catalog', {})) @staticmethod def normalize_interface(interface): if interface: interface = interface.rstrip('URL') return interface def is_interface_match(self, endpoint, interface): try: return interface == endpoint['interface'] except KeyError: return False @positional() def get_urls(self, service_type=None, interface='publicURL', region_name=None, service_name=None, service_id=None, endpoint_id=None): endpoints = self._get_service_endpoints(service_type=service_type, interface=interface, region_name=region_name, service_name=service_name, service_id=service_id, endpoint_id=endpoint_id) return tuple([endpoint['url'] for endpoint in endpoints]) keystoneauth-2.4.1/keystoneauth1/access/service_providers.py000066400000000000000000000030661271245473000244270ustar00rootroot00000000000000# 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 keystoneauth1 import exceptions class ServiceProviders(object): """Helper methods for dealing with Service Providers.""" @classmethod def from_token(cls, token): if 'token' not in token: raise ValueError('Token format does not support service' 'providers.') return cls(token['token'].get('service_providers', [])) def __init__(self, service_providers): def normalize(service_providers_list): return dict((sp['id'], sp) for sp in service_providers_list if 'id' in sp) self._service_providers = normalize(service_providers) def _get_service_provider(self, sp_id): try: return self._service_providers[sp_id] except KeyError: raise exceptions.ServiceProviderNotFound(sp_id) def get_sp_url(self, sp_id): return self._get_service_provider(sp_id).get('sp_url') def get_auth_url(self, sp_id): return self._get_service_provider(sp_id).get('auth_url') keystoneauth-2.4.1/keystoneauth1/adapter.py000066400000000000000000000331221271245473000210450ustar00rootroot00000000000000# 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 from positional import positional class Adapter(object): """An instance of a session with local variables. A session is a global object that is shared around amongst many clients. It therefore contains state that is relevant to everyone. There is a lot of state such as the service type and region_name that are only relevant to a particular client that is using the session. An adapter provides a wrapper of client local data around the global session object. :param session: The session object to wrap. :type session: keystoneauth1.session.Session :param str service_type: The default service_type for URL discovery. :param str service_name: The default service_name for URL discovery. :param str interface: The default interface for URL discovery. :param str region_name: The default region_name for URL discovery. :param str endpoint_override: Always use this endpoint URL for requests for this client. :param tuple version: The version that this API targets. :param auth: An auth plugin to use instead of the session one. :type auth: keystoneauth1.plugin.BaseAuthPlugin :param str user_agent: The User-Agent string to set. :param int connect_retries: the maximum number of retries that should be attempted for connection errors. Default None - use session default which is don't retry. :param logger: A logging object to use for requests that pass through this adapter. :type logger: logging.Logger """ @positional() def __init__(self, session, service_type=None, service_name=None, interface=None, region_name=None, endpoint_override=None, version=None, auth=None, user_agent=None, connect_retries=None, logger=None): # NOTE(jamielennox): when adding new parameters to adapter please also # add them to the adapter call in httpclient.HTTPClient.__init__ as # well as to load_adapter_from_argparse below if the argument is # intended to be something a user would reasonably expect to set on # a command line self.session = session self.service_type = service_type self.service_name = service_name self.interface = interface self.region_name = region_name self.endpoint_override = endpoint_override self.version = version self.user_agent = user_agent self.auth = auth self.connect_retries = connect_retries self.logger = logger def _set_endpoint_filter_kwargs(self, kwargs): if self.service_type: kwargs.setdefault('service_type', self.service_type) if self.service_name: kwargs.setdefault('service_name', self.service_name) if self.interface: kwargs.setdefault('interface', self.interface) if self.region_name: kwargs.setdefault('region_name', self.region_name) if self.version: kwargs.setdefault('version', self.version) def request(self, url, method, **kwargs): endpoint_filter = kwargs.setdefault('endpoint_filter', {}) self._set_endpoint_filter_kwargs(endpoint_filter) if self.endpoint_override: kwargs.setdefault('endpoint_override', self.endpoint_override) if self.auth: kwargs.setdefault('auth', self.auth) if self.user_agent: kwargs.setdefault('user_agent', self.user_agent) if self.connect_retries is not None: kwargs.setdefault('connect_retries', self.connect_retries) if self.logger: kwargs.setdefault('logger', self.logger) return self.session.request(url, method, **kwargs) def get_token(self, auth=None): """Return a token as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :returns: A valid token. :rtype: :class:`str` """ return self.session.get_token(auth or self.auth) def get_endpoint(self, auth=None, **kwargs): """Get an endpoint as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :returns: An endpoint if available or None. :rtype: :class:`str` """ if self.endpoint_override: return self.endpoint_override self._set_endpoint_filter_kwargs(kwargs) return self.session.get_endpoint(auth or self.auth, **kwargs) def invalidate(self, auth=None): """Invalidate an authentication plugin.""" return self.session.invalidate(auth or self.auth) def get_user_id(self, auth=None): """Return the authenticated user_id as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :returns: Current `user_id` or None if not supported by plugin. :rtype: :class:`str` """ return self.session.get_user_id(auth or self.auth) def get_project_id(self, auth=None): """Return the authenticated project_id as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :returns: Current `project_id` or None if not supported by plugin. :rtype: :class:`str` """ return self.session.get_project_id(auth or self.auth) def get(self, url, **kwargs): return self.request(url, 'GET', **kwargs) def head(self, url, **kwargs): return self.request(url, 'HEAD', **kwargs) def post(self, url, **kwargs): return self.request(url, 'POST', **kwargs) def put(self, url, **kwargs): return self.request(url, 'PUT', **kwargs) def patch(self, url, **kwargs): return self.request(url, 'PATCH', **kwargs) def delete(self, url, **kwargs): return self.request(url, 'DELETE', **kwargs) @classmethod def register_argparse_arguments(cls, parser, service_type=None): """Attach arguments to a given argparse Parser for Adapters :param parser: The argparse parser to attach options to. :type parser: argparse.ArgumentParser :param str service_type: Default service_type value. (optional) """ adapter_group = parser.add_argument_group( 'Service Options', 'Options controlling the specialization of the API' ' Connection from information found in the catalog') adapter_group.add_argument( '--os-service-type', metavar='', default=os.environ.get('OS_SERVICE_TYPE', service_type), help='Service type to request from the catalog') adapter_group.add_argument( '--os-service-name', metavar='', default=os.environ.get('OS_SERVICE_NAME', None), help='Service name to request from the catalog') adapter_group.add_argument( '--os-interface', metavar='', default=os.environ.get('OS_INTERFACE', 'public'), help='API Interface to use [public, internal, admin]') adapter_group.add_argument( '--os-region-name', metavar='', default=os.environ.get('OS_REGION_NAME', None), help='Region of the cloud to use') adapter_group.add_argument( '--os-endpoint-override', metavar='', default=os.environ.get('OS_ENDPOINT_OVERRIDE', None), help='Endpoint to use instead of the endpoint in the catalog') adapter_group.add_argument( '--os-api-version', metavar='', default=os.environ.get('OS_API_VERSION', None), help='Which version of the service API to use') @classmethod def register_service_argparse_arguments(cls, parser, service_type): """Attach arguments to a given argparse Parser for Adapters :param parser: The argparse parser to attach options to. :type parser: argparse.ArgumentParser :param str service_type: Name of a service to generate additional arguments for. """ service_env = service_type.upper().replace('-', '_') adapter_group = parser.add_argument_group( '{service_type} Service Options'.format( service_type=service_type.title()), 'Options controlling the specialization of the {service_type}' ' API Connection from information found in the catalog'.format( service_type=service_type.title())) adapter_group.add_argument( '--os-{service_type}-service-type'.format( service_type=service_type), metavar='', default=os.environ.get( 'OS_{service_type}_SERVICE_TYPE'.format( service_type=service_env), None), help=('Service type to request from the catalog for the' ' {service_type} service'.format( service_type=service_type))) adapter_group.add_argument( '--os-{service_type}-service-name'.format( service_type=service_type), metavar='', default=os.environ.get( 'OS_{service_type}_SERVICE_NAME'.format( service_type=service_env), None), help=('Service name to request from the catalog for the' ' {service_type} service'.format( service_type=service_type))) adapter_group.add_argument( '--os-{service_type}-interface'.format( service_type=service_type), metavar='', default=os.environ.get( 'OS_{service_type}_INTERFACE'.format( service_type=service_env), None), help=('API Interface to use for the {service_type} service' ' [public, internal, admin]'.format( service_type=service_type))) adapter_group.add_argument( '--os-{service_type}-api-version'.format( service_type=service_type), metavar='', default=os.environ.get( 'OS_{service_type}_API_VERSION'.format( service_type=service_env), None), help=('Which version of the service API to use for' ' the {service_type} service'.format( service_type=service_type))) adapter_group.add_argument( '--os-{service_type}-endpoint-override'.format( service_type=service_type), metavar='', default=os.environ.get( 'OS_{service_type}_ENDPOINT_OVERRIDE'.format( service_type=service_env), None), help=('Endpoint to use for the {service_type} service' ' instead of the endpoint in the catalog'.format( service_type=service_type))) class LegacyJsonAdapter(Adapter): """Make something that looks like an old HTTPClient. A common case when using an adapter is that we want an interface similar to the HTTPClients of old which returned the body as JSON as well. You probably don't want this if you are starting from scratch. """ def request(self, *args, **kwargs): headers = kwargs.setdefault('headers', {}) headers.setdefault('Accept', 'application/json') try: kwargs['json'] = kwargs.pop('body') except KeyError: pass resp = super(LegacyJsonAdapter, self).request(*args, **kwargs) try: body = resp.json() except ValueError: body = None return resp, body def register_adapter_argparse_arguments(*args, **kwargs): return Adapter.register_argparse_arguments(*args, **kwargs) def register_service_adapter_argparse_arguments(*args, **kwargs): return Adapter.register_service_argparse_arguments(*args, **kwargs) keystoneauth-2.4.1/keystoneauth1/discover.py000066400000000000000000000304501271245473000212440ustar00rootroot00000000000000# 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. """The passive components to version discovery. The Discover object in discover.py contains functions that can create objects on your behalf. These functions are not usable from within the keystoneauth1 library because you will get dependency resolution issues. The Discover object in this file provides the querying components of Discovery. This includes functions like url_for which allow you to retrieve URLs and the raw data specified in version discovery responses. """ import re from positional import positional from keystoneauth1 import _utils as utils from keystoneauth1 import exceptions _LOGGER = utils.get_logger(__name__) @positional() def get_version_data(session, url, authenticated=None): """Retrieve raw version data from a url.""" headers = {'Accept': 'application/json'} resp = session.get(url, headers=headers, authenticated=authenticated) try: body_resp = resp.json() except ValueError: pass else: # In the event of querying a root URL we will get back a list of # available versions. try: return body_resp['versions']['values'] except (KeyError, TypeError): pass # Most servers don't have a 'values' element so accept a simple # versions dict if available. try: return body_resp['versions'] except KeyError: pass # Otherwise if we query an endpoint like /v2.0 then we will get back # just the one available version. try: return [body_resp['version']] except KeyError: pass err_text = resp.text[:50] + '...' if len(resp.text) > 50 else resp.text raise exceptions.DiscoveryFailure('Invalid Response - Bad version data ' 'returned: %s' % err_text) def normalize_version_number(version): """Turn a version representation into a tuple.""" # trim the v from a 'v2.0' or similar try: version = version.lstrip('v') except AttributeError: pass # if it's an integer or a numeric as a string then normalize it # to a string, this ensures 1 decimal point try: num = float(version) except Exception: pass else: version = str(num) # if it's a string (or an integer) from above break it on . try: return tuple(map(int, version.split('.'))) except Exception: pass # last attempt, maybe it's a list or iterable. try: return tuple(map(int, version)) except Exception: pass raise TypeError('Invalid version specified: %s' % version) def version_match(required, candidate): """Test that an available version satisfies the required version. To be suitable a version must be of the same major version as required and be at least a match in minor/patch level. eg. 3.3 is a match for a required 3.1 but 4.1 is not. :param tuple required: the version that must be met. :param tuple candidate: the version to test against required. :returns: True if candidate is suitable False otherwise. :rtype: bool """ # major versions must be the same (e.g. even though v2 is a lower # version than v3 we can't use it if v2 was requested) if candidate[0] != required[0]: return False # prevent selecting a minor version less than what is required if candidate < required: return False return True class Discover(object): CURRENT_STATUSES = ('stable', 'current', 'supported') DEPRECATED_STATUSES = ('deprecated',) EXPERIMENTAL_STATUSES = ('experimental',) @positional() def __init__(self, session, url, authenticated=None): self._data = get_version_data(session, url, authenticated=authenticated) def raw_version_data(self, allow_experimental=False, allow_deprecated=True, allow_unknown=False): """Get raw version information from URL. Raw data indicates that only minimal validation processing is performed on the data, so what is returned here will be the data in the same format it was received from the endpoint. :param bool allow_experimental: Allow experimental version endpoints. :param bool allow_deprecated: Allow deprecated version endpoints. :param bool allow_unknown: Allow endpoints with an unrecognised status. :returns: The endpoints returned from the server that match the criteria. :rtype: list """ versions = [] for v in self._data: try: status = v['status'] except KeyError: _LOGGER.warning('Skipping over invalid version data. ' 'No stability status in version.') continue status = status.lower() if status in self.CURRENT_STATUSES: versions.append(v) elif status in self.DEPRECATED_STATUSES: if allow_deprecated: versions.append(v) elif status in self.EXPERIMENTAL_STATUSES: if allow_experimental: versions.append(v) elif allow_unknown: versions.append(v) return versions @positional() def version_data(self, reverse=False, **kwargs): """Get normalized version data. Return version data in a structured way. :param bool reverse: Reverse the list. reverse=true will mean the returned list is sorted from newest to oldest version. :returns: A list of version data dictionaries sorted by version number. Each data element in the returned list is a dictionary consisting of at least: :version tuple: The normalized version of the endpoint. :url str: The url for the endpoint. :raw_status str: The status as provided by the server :rtype: list(dict) """ data = self.raw_version_data(**kwargs) versions = [] for v in data: try: version_str = v['id'] except KeyError: _LOGGER.info('Skipping invalid version data. Missing ID.') continue try: links = v['links'] except KeyError: _LOGGER.info('Skipping invalid version data. Missing links') continue version_number = normalize_version_number(version_str) for link in links: try: rel = link['rel'] url = link['href'] except (KeyError, TypeError): _LOGGER.info('Skipping invalid version link. ' 'Missing link URL or relationship.') continue if rel.lower() == 'self': break else: _LOGGER.info('Skipping invalid version data. ' 'Missing link to endpoint.') continue versions.append({'version': version_number, 'url': url, 'raw_status': v['status']}) versions.sort(key=lambda v: v['version'], reverse=reverse) return versions def data_for(self, version, **kwargs): """Return endpoint data for a version. :param tuple version: The version is always a minimum version in the same major release as there should be no compatibility issues with using a version newer than the one asked for. :returns: the endpoint data for a URL that matches the required version (the format is described in version_data) or None if no match. :rtype: dict """ version = normalize_version_number(version) for data in self.version_data(reverse=True, **kwargs): if version_match(version, data['version']): return data return None def url_for(self, version, **kwargs): """Get the endpoint url for a version. :param tuple version: The version is always a minimum version in the same major release as there should be no compatibility issues with using a version newer than the one asked for. :returns: The url for the specified version or None if no match. :rtype: str """ data = self.data_for(version, **kwargs) return data['url'] if data else None class _VersionHacks(object): """A container to abstract the list of version hacks. This could be done as simply a dictionary but is abstracted like this to make for easier testing. """ def __init__(self): self._discovery_data = {} def add_discover_hack(self, service_type, old, new=''): """Add a new hack for a service type. :param str service_type: The service_type in the catalog. :param re.RegexObject old: The pattern to use. :param str new: What to replace the pattern with. """ hacks = self._discovery_data.setdefault(service_type, []) hacks.append((old, new)) def get_discover_hack(self, service_type, url): """Apply the catalog hacks and figure out an unversioned endpoint. :param str service_type: the service_type to look up. :param str url: The original url that came from a service_catalog. :returns: Either the unversioned url or the one from the catalog to try. """ for old, new in self._discovery_data.get(service_type, []): new_string, number_of_subs_made = old.subn(new, url) if number_of_subs_made > 0: return new_string return url _VERSION_HACKS = _VersionHacks() _VERSION_HACKS.add_discover_hack('identity', re.compile('/v2.0/?$'), '/') def _get_catalog_discover_hack(service_type, url): """Apply the catalog hacks and figure out an unversioned endpoint. This function is internal to keystoneauth1. :param str service_type: the service_type to look up. :param str url: The original url that came from a service_catalog. :returns: Either the unversioned url or the one from the catalog to try. """ return _VERSION_HACKS.get_discover_hack(service_type, url) def add_catalog_discover_hack(service_type, old, new): """Adds a version removal rule for a particular service. Originally deployments of OpenStack would contain a versioned endpoint in the catalog for different services. E.g. an identity service might look like ``http://localhost:5000/v2.0``. This is a problem when we want to use a different version like v3.0 as there is no way to tell where it is located. We cannot simply change all service catalogs either so there must be a way to handle the older style of catalog. This function adds a rule for a given service type that if part of the URL matches a given regular expression in *old* then it will be replaced with the *new* value. This will replace all instances of old with new. It should therefore contain a regex anchor. For example the included rule states:: add_catalog_version_hack('identity', re.compile('/v2.0/?$'), '/') so if the catalog retrieves an *identity* URL that ends with /v2.0 or /v2.0/ then it should replace it simply with / to fix the user's catalog. :param str service_type: The service type as defined in the catalog that the rule will apply to. :param re.RegexObject old: The regular expression to search for and replace if found. :param str new: The new string to replace the pattern with. """ _VERSION_HACKS.add_discover_hack(service_type, old, new) keystoneauth-2.4.1/keystoneauth1/exceptions/000077500000000000000000000000001271245473000212335ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/exceptions/__init__.py000066400000000000000000000020361271245473000233450ustar00rootroot00000000000000# 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 keystoneauth1.exceptions.auth import * # noqa from keystoneauth1.exceptions.auth_plugins import * # noqa from keystoneauth1.exceptions.base import * # noqa from keystoneauth1.exceptions.catalog import * # noqa from keystoneauth1.exceptions.connection import * # noqa from keystoneauth1.exceptions.discovery import * # noqa from keystoneauth1.exceptions.http import * # noqa from keystoneauth1.exceptions.response import * # noqa from keystoneauth1.exceptions.service_providers import * # noqa keystoneauth-2.4.1/keystoneauth1/exceptions/auth.py000066400000000000000000000012551271245473000225510ustar00rootroot00000000000000# 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 keystoneauth1.exceptions import base class AuthorizationFailure(base.ClientException): message = "Cannot authorize API client." keystoneauth-2.4.1/keystoneauth1/exceptions/auth_plugins.py000066400000000000000000000055641271245473000243210ustar00rootroot00000000000000# 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 keystoneauth1.exceptions import base __all__ = ('AuthPluginException', 'MissingAuthPlugin', 'NoMatchingPlugin', 'UnsupportedParameters', 'OptionError', 'MissingRequiredOptions') class AuthPluginException(base.ClientException): message = "Unknown error with authentication plugins." class MissingAuthPlugin(AuthPluginException): message = "An authenticated request is required but no plugin available." class NoMatchingPlugin(AuthPluginException): """No auth plugins could be created from the parameters provided. :param str name: The name of the plugin that was attempted to load. .. py:attribute:: name The name of the plugin that was attempted to load. """ def __init__(self, name): self.name = name msg = 'The plugin %s could not be found' % name super(NoMatchingPlugin, self).__init__(msg) class UnsupportedParameters(AuthPluginException): """A parameter that was provided or returned is not supported. :param list(str) names: Names of the unsupported parameters. .. py:attribute:: names Names of the unsupported parameters. """ def __init__(self, names): self.names = names m = 'The following parameters were given that are unsupported: %s' super(UnsupportedParameters, self).__init__(m % ', '.join(self.names)) class OptionError(AuthPluginException): """A requirement of this plugin loader was not met. This error can be raised by a specific plugin loader during the load_from_options stage to indicate a parameter problem that can not be handled by the generic options loader. The intention here is that a plugin can do checks like if a name parameter is provided then a domain parameter must also be provided, but that Opt checking doesn't handle. """ class MissingRequiredOptions(OptionError): """One or more required options were not provided. :param list(keystoneauth1.loading.Opt) options: Missing options. .. py:attribute:: options List of the missing options. """ def __init__(self, options): self.options = options names = ", ".join(o.dest for o in options) m = 'Auth plugin requires parameters which were not given: %s' super(MissingRequiredOptions, self).__init__(m % names) keystoneauth-2.4.1/keystoneauth1/exceptions/base.py000066400000000000000000000016611271245473000225230ustar00rootroot00000000000000# 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__ = ('ClientException',) class ClientException(Exception): """The base exception for everything to do with clients.""" message = None def __init__(self, message=None): if not message: if self.message: message = self.message else: message = self.__class__.__name__ super(Exception, self).__init__(message) keystoneauth-2.4.1/keystoneauth1/exceptions/catalog.py000066400000000000000000000017231271245473000232220ustar00rootroot00000000000000# 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 keystoneauth1.exceptions import base __all__ = ('CatalogException', 'EmptyCatalog', 'EndpointNotFound') class CatalogException(base.ClientException): message = "Unknown error with service catalog." class EndpointNotFound(CatalogException): message = "Could not find requested endpoint in Service Catalog." class EmptyCatalog(EndpointNotFound): message = "The service catalog is empty." keystoneauth-2.4.1/keystoneauth1/exceptions/connection.py000066400000000000000000000030371271245473000237470ustar00rootroot00000000000000# 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 keystoneauth1.exceptions import base __all__ = ('ConnectionError', 'ConnectTimeout', 'ConnectFailure', 'SSLError', 'RetriableConnectionFailure', 'UnknownConnectionError') class RetriableConnectionFailure(Exception): """A mixin class that implies you can retry the most recent request.""" pass class ConnectionError(base.ClientException): message = "Cannot connect to API service." class ConnectTimeout(ConnectionError, RetriableConnectionFailure): message = "Timed out connecting to service." class ConnectFailure(ConnectionError, RetriableConnectionFailure): message = "Connection failure that may be retried." class SSLError(ConnectionError): message = "An SSL error occurred." class UnknownConnectionError(ConnectionError): """An error was encountered but we don't know what it is.""" def __init__(self, msg, original): super(UnknownConnectionError, self).__init__(msg) self.original = original keystoneauth-2.4.1/keystoneauth1/exceptions/discovery.py000066400000000000000000000015511271245473000236160ustar00rootroot00000000000000# 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 keystoneauth1.exceptions import base __all__ = ('DiscoveryFailure', 'VersionNotAvailable') class DiscoveryFailure(base.ClientException): message = "Discovery of client versions failed." class VersionNotAvailable(DiscoveryFailure): message = "Discovery failed. Requested version is not available." keystoneauth-2.4.1/keystoneauth1/exceptions/http.py000066400000000000000000000251331271245473000225700ustar00rootroot00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Nebula, Inc. # Copyright 2013 Alessio Ababilov # Copyright 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. """ HTTP Exceptions used by keystoneauth1 """ import inspect import sys import six from keystoneauth1.exceptions import base __all__ = ('HttpError', 'HTTPClientError', 'BadRequest', 'Unauthorized', 'PaymentRequired', 'Forbidden', 'NotFound', 'MethodNotAllowed', 'NotAcceptable', 'ProxyAuthenticationRequired', 'RequestTimeout', 'Conflict', 'Gone', 'LengthRequired', 'PreconditionFailed', 'RequestEntityTooLarge', 'RequestUriTooLong', 'UnsupportedMediaType', 'RequestedRangeNotSatisfiable', 'ExpectationFailed', 'UnprocessableEntity', 'HttpServerError', 'InternalServerError', 'HttpNotImplemented', 'BadGateway', 'ServiceUnavailable', 'GatewayTimeout', 'HttpVersionNotSupported', 'from_response') class HttpError(base.ClientException): """The base exception class for all HTTP exceptions.""" http_status = 0 message = "HTTP Error" def __init__(self, message=None, details=None, response=None, request_id=None, url=None, method=None, http_status=None, retry_after=0): self.http_status = http_status or self.http_status self.message = message or self.message self.details = details self.request_id = request_id self.response = response self.url = url self.method = method formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) self.retry_after = retry_after if request_id: formatted_string += " (Request-ID: %s)" % request_id super(HttpError, self).__init__(formatted_string) class HTTPClientError(HttpError): """Client-side HTTP error. Exception for cases in which the client seems to have erred. """ message = "HTTP Client Error" class HttpServerError(HttpError): """Server-side HTTP error. Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ message = "HTTP Server Error" class BadRequest(HTTPClientError): """HTTP 400 - Bad Request. The request cannot be fulfilled due to bad syntax. """ http_status = 400 message = "Bad Request" class Unauthorized(HTTPClientError): """HTTP 401 - Unauthorized. Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. """ http_status = 401 message = "Unauthorized" class PaymentRequired(HTTPClientError): """HTTP 402 - Payment Required. Reserved for future use. """ http_status = 402 message = "Payment Required" class Forbidden(HTTPClientError): """HTTP 403 - Forbidden. The request was a valid request, but the server is refusing to respond to it. """ http_status = 403 message = "Forbidden" class NotFound(HTTPClientError): """HTTP 404 - Not Found. The requested resource could not be found but may be available again in the future. """ http_status = 404 message = "Not Found" class MethodNotAllowed(HTTPClientError): """HTTP 405 - Method Not Allowed. A request was made of a resource using a request method not supported by that resource. """ http_status = 405 message = "Method Not Allowed" class NotAcceptable(HTTPClientError): """HTTP 406 - Not Acceptable. The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request. """ http_status = 406 message = "Not Acceptable" class ProxyAuthenticationRequired(HTTPClientError): """HTTP 407 - Proxy Authentication Required. The client must first authenticate itself with the proxy. """ http_status = 407 message = "Proxy Authentication Required" class RequestTimeout(HTTPClientError): """HTTP 408 - Request Timeout. The server timed out waiting for the request. """ http_status = 408 message = "Request Timeout" class Conflict(HTTPClientError): """HTTP 409 - Conflict. Indicates that the request could not be processed because of conflict in the request, such as an edit conflict. """ http_status = 409 message = "Conflict" class Gone(HTTPClientError): """HTTP 410 - Gone. Indicates that the resource requested is no longer available and will not be available again. """ http_status = 410 message = "Gone" class LengthRequired(HTTPClientError): """HTTP 411 - Length Required. The request did not specify the length of its content, which is required by the requested resource. """ http_status = 411 message = "Length Required" class PreconditionFailed(HTTPClientError): """HTTP 412 - Precondition Failed. The server does not meet one of the preconditions that the requester put on the request. """ http_status = 412 message = "Precondition Failed" class RequestEntityTooLarge(HTTPClientError): """HTTP 413 - Request Entity Too Large. The request is larger than the server is willing or able to process. """ http_status = 413 message = "Request Entity Too Large" def __init__(self, *args, **kwargs): try: self.retry_after = int(kwargs.pop('retry_after')) except (KeyError, ValueError): self.retry_after = 0 super(RequestEntityTooLarge, self).__init__(*args, **kwargs) class RequestUriTooLong(HTTPClientError): """HTTP 414 - Request-URI Too Long. The URI provided was too long for the server to process. """ http_status = 414 message = "Request-URI Too Long" class UnsupportedMediaType(HTTPClientError): """HTTP 415 - Unsupported Media Type. The request entity has a media type which the server or resource does not support. """ http_status = 415 message = "Unsupported Media Type" class RequestedRangeNotSatisfiable(HTTPClientError): """HTTP 416 - Requested Range Not Satisfiable. The client has asked for a portion of the file, but the server cannot supply that portion. """ http_status = 416 message = "Requested Range Not Satisfiable" class ExpectationFailed(HTTPClientError): """HTTP 417 - Expectation Failed. The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 message = "Expectation Failed" class UnprocessableEntity(HTTPClientError): """HTTP 422 - Unprocessable Entity. The request was well-formed but was unable to be followed due to semantic errors. """ http_status = 422 message = "Unprocessable Entity" class InternalServerError(HttpServerError): """HTTP 500 - Internal Server Error. A generic error message, given when no more specific message is suitable. """ http_status = 500 message = "Internal Server Error" # NotImplemented is a python keyword. class HttpNotImplemented(HttpServerError): """HTTP 501 - Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request. """ http_status = 501 message = "Not Implemented" class BadGateway(HttpServerError): """HTTP 502 - Bad Gateway. The server was acting as a gateway or proxy and received an invalid response from the upstream server. """ http_status = 502 message = "Bad Gateway" class ServiceUnavailable(HttpServerError): """HTTP 503 - Service Unavailable. The server is currently unavailable. """ http_status = 503 message = "Service Unavailable" class GatewayTimeout(HttpServerError): """HTTP 504 - Gateway Timeout. The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. """ http_status = 504 message = "Gateway Timeout" class HttpVersionNotSupported(HttpServerError): """HTTP 505 - HttpVersion Not Supported. The server does not support the HTTP protocol version used in the request. """ http_status = 505 message = "HTTP Version Not Supported" # _code_map contains all the classes that have http_status attribute. _code_map = dict( (getattr(obj, 'http_status', None), obj) for name, obj in six.iteritems(vars(sys.modules[__name__])) if inspect.isclass(obj) and getattr(obj, 'http_status', False) ) def from_response(response, method, url): """Returns an instance of :class:`HttpError` or subclass based on response. :param response: instance of `requests.Response` class :param method: HTTP method used for request :param url: URL used for request """ req_id = response.headers.get("x-openstack-request-id") kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, "request_id": req_id, } if "retry-after" in response.headers: kwargs["retry_after"] = response.headers["retry-after"] content_type = response.headers.get("Content-Type", "") if content_type.startswith("application/json"): try: body = response.json() except ValueError: pass else: if isinstance(body, dict) and isinstance(body.get("error"), dict): error = body["error"] kwargs["message"] = error.get("message") kwargs["details"] = error.get("details") elif content_type.startswith("text/"): kwargs["details"] = response.text try: cls = _code_map[response.status_code] except KeyError: if 500 <= response.status_code < 600: cls = HttpServerError elif 400 <= response.status_code < 500: cls = HTTPClientError else: cls = HttpError return cls(**kwargs) keystoneauth-2.4.1/keystoneauth1/exceptions/response.py000066400000000000000000000014771271245473000234540ustar00rootroot00000000000000# 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 keystoneauth1.exceptions import base __all__ = ('InvalidResponse',) class InvalidResponse(base.ClientException): message = "Invalid response from server." def __init__(self, response): super(InvalidResponse, self).__init__() self.response = response keystoneauth-2.4.1/keystoneauth1/exceptions/service_providers.py000066400000000000000000000016351271245473000253470ustar00rootroot00000000000000# 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 keystoneauth1.exceptions import base __all__ = ('ServiceProviderNotFound',) class ServiceProviderNotFound(base.ClientException): """A Service Provider cannot be found""" def __init__(self, sp_id): self.sp_id = sp_id msg = 'The Service Provider %(sp)s could not be found' % {'sp': sp_id} super(ServiceProviderNotFound, self).__init__(msg) keystoneauth-2.4.1/keystoneauth1/extras/000077500000000000000000000000001271245473000203605ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/extras/__init__.py000066400000000000000000000015441271245473000224750ustar00rootroot00000000000000# 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(jamielennox): This directory is designed to reflect the dependency # extras in the setup.cfg file. If you create an additional dependency section # like 'kerberos' in the setup.cfg it is expected that there be a kerberos # package here that can be imported. # # e.g. from keystoneauth1.extras import kerberos pass keystoneauth-2.4.1/keystoneauth1/extras/_saml2/000077500000000000000000000000001271245473000215355ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/extras/_saml2/__init__.py000066400000000000000000000013041271245473000236440ustar00rootroot00000000000000# 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 keystoneauth1.extras._saml2 import v3 V3Saml2Password = v3.Saml2Password V3ADFSPassword = v3.ADFSPassword __all__ = ('V3Saml2Password', 'V3ADFSPassword') keystoneauth-2.4.1/keystoneauth1/extras/_saml2/_loading.py000066400000000000000000000033031271245473000236620ustar00rootroot00000000000000# 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 keystoneauth1.extras import _saml2 from keystoneauth1 import loading class Saml2Password(loading.BaseFederationLoader): @property def plugin_class(self): return _saml2.V3Saml2Password def get_options(self): options = super(Saml2Password, self).get_options() options.extend([ loading.Opt('identity-provider-url', help=('An Identity Provider URL, where the SAML2 ' 'authentication request will be sent.')), loading.Opt('username', help='Username'), loading.Opt('password', secret=True, help='Password') ]) return options class ADFSPassword(loading.BaseFederationLoader): @property def plugin_class(self): return _saml2.V3ADFSPassword def get_options(self): options = super(ADFSPassword, self).get_options() options.extend([ loading.Opt('service-provider-endpoint', help="Service Provider's Endpoint"), loading.Opt('username', help='Username'), loading.Opt('password', secret=True, help='Password') ]) return options keystoneauth-2.4.1/keystoneauth1/extras/_saml2/v3/000077500000000000000000000000001271245473000220655ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/extras/_saml2/v3/__init__.py000066400000000000000000000013551271245473000242020ustar00rootroot00000000000000# 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 keystoneauth1.extras._saml2.v3 import adfs from keystoneauth1.extras._saml2.v3 import saml2 Saml2Password = saml2.Password ADFSPassword = adfs.Password __all__ = ('Saml2Password', 'ADFSPassword') keystoneauth-2.4.1/keystoneauth1/extras/_saml2/v3/adfs.py000066400000000000000000000436541271245473000233700ustar00rootroot00000000000000# 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 uuid from lxml import etree from six.moves import urllib from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.extras._saml2.v3 import base class Password(base.BaseSAMLPlugin): """Authentication plugin for Microsoft ADFS2.0 IdPs.""" DEFAULT_ADFS_TOKEN_EXPIRATION = 120 HEADER_SOAP = {"Content-Type": "application/soap+xml; charset=utf-8"} HEADER_X_FORM = {"Content-Type": "application/x-www-form-urlencoded"} NAMESPACES = { 's': 'http://www.w3.org/2003/05/soap-envelope', 'a': 'http://www.w3.org/2005/08/addressing', 'u': ('http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-utility-1.0.xsd') } ADFS_TOKEN_NAMESPACES = { 's': 'http://www.w3.org/2003/05/soap-envelope', 't': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512' } ADFS_ASSERTION_XPATH = ('/s:Envelope/s:Body' '/t:RequestSecurityTokenResponseCollection' '/t:RequestSecurityTokenResponse') def __init__(self, auth_url, identity_provider, identity_provider_url, service_provider_endpoint, username, password, protocol, **kwargs): """Constructor for ``ADFSPassword``. :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: name of the Identity Provider the client will authenticate against. This parameter will be used to build a dynamic URL used to obtain unscoped OpenStack token. :type identity_provider: string :param identity_provider_url: An Identity Provider URL, where the SAML2 authentication request will be sent. :type identity_provider_url: string :param service_provider_endpoint: Endpoint where an assertion is being sent, for instance: ``https://host.domain/Shibboleth.sso/ADFS`` :type service_provider_endpoint: string :param username: User's login :type username: string :param password: User's password :type password: string """ super(Password, self).__init__( auth_url=auth_url, identity_provider=identity_provider, identity_provider_url=identity_provider_url, username=username, password=password, protocol=protocol) self.service_provider_endpoint = service_provider_endpoint def _cookies(self, session): """Check if cookie jar is not empty. keystoneauth1.session.Session object doesn't have a cookies attribute. We should then try fetching cookies from the underlying requests.Session object. If that fails too, there is something wrong and let Python raise the AttributeError. :param session :returns: True if cookie jar is nonempty, False otherwise :raises AttributeError: in case cookies are not find anywhere """ try: return bool(session.cookies) except AttributeError: pass return bool(session.session.cookies) def _token_dates(self, fmt='%Y-%m-%dT%H:%M:%S.%fZ'): """Calculate created and expires datetime objects. The method is going to be used for building ADFS Request Security Token message. Time interval between ``created`` and ``expires`` dates is now static and equals to 120 seconds. ADFS security tokens should not be live too long, as currently ``keystoneauth1`` doesn't have mechanisms for reusing such tokens (every time ADFS authn method is called, keystoneauth1 will login with the ADFS instance). :param fmt: Datetime format for specifying string format of a date. It should not be changed if the method is going to be used for building the ADFS security token request. :type fmt: string """ date_created = datetime.datetime.utcnow() date_expires = date_created + datetime.timedelta( seconds=self.DEFAULT_ADFS_TOKEN_EXPIRATION) return [_time.strftime(fmt) for _time in (date_created, date_expires)] def _prepare_adfs_request(self): """Build the ADFS Request Security Token SOAP message. Some values like username or password are inserted in the request. """ WSS_SECURITY_NAMESPACE = { 'o': ('http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-secext-1.0.xsd') } TRUST_NAMESPACE = { 'trust': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512' } WSP_NAMESPACE = { 'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy' } WSA_NAMESPACE = { 'wsa': 'http://www.w3.org/2005/08/addressing' } root = etree.Element( '{http://www.w3.org/2003/05/soap-envelope}Envelope', nsmap=self.NAMESPACES) header = etree.SubElement( root, '{http://www.w3.org/2003/05/soap-envelope}Header') action = etree.SubElement( header, "{http://www.w3.org/2005/08/addressing}Action") action.set( "{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1") action.text = ('http://docs.oasis-open.org/ws-sx/ws-trust/200512' '/RST/Issue') messageID = etree.SubElement( header, '{http://www.w3.org/2005/08/addressing}MessageID') messageID.text = 'urn:uuid:' + uuid.uuid4().hex replyID = etree.SubElement( header, '{http://www.w3.org/2005/08/addressing}ReplyTo') address = etree.SubElement( replyID, '{http://www.w3.org/2005/08/addressing}Address') address.text = 'http://www.w3.org/2005/08/addressing/anonymous' to = etree.SubElement( header, '{http://www.w3.org/2005/08/addressing}To') to.set("{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1") security = etree.SubElement( header, '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-secext-1.0.xsd}Security', nsmap=WSS_SECURITY_NAMESPACE) security.set( "{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1") timestamp = etree.SubElement( security, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-utility-1.0.xsd}Timestamp')) timestamp.set( ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-utility-1.0.xsd}Id'), '_0') created = etree.SubElement( timestamp, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-utility-1.0.xsd}Created')) expires = etree.SubElement( timestamp, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-utility-1.0.xsd}Expires')) created.text, expires.text = self._token_dates() usernametoken = etree.SubElement( security, '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-secext-1.0.xsd}UsernameToken') usernametoken.set( ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-' 'wssecurity-utility-1.0.xsd}u'), "uuid-%s-1" % uuid.uuid4().hex) username = etree.SubElement( usernametoken, ('{http://docs.oasis-open.org/wss/2004/01/oasis-' '200401-wss-wssecurity-secext-1.0.xsd}Username')) password = etree.SubElement( usernametoken, ('{http://docs.oasis-open.org/wss/2004/01/oasis-' '200401-wss-wssecurity-secext-1.0.xsd}Password'), Type=('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-' 'username-token-profile-1.0#PasswordText')) body = etree.SubElement( root, "{http://www.w3.org/2003/05/soap-envelope}Body") request_security_token = etree.SubElement( body, ('{http://docs.oasis-open.org/ws-sx/ws-trust/200512}' 'RequestSecurityToken'), nsmap=TRUST_NAMESPACE) applies_to = etree.SubElement( request_security_token, '{http://schemas.xmlsoap.org/ws/2004/09/policy}AppliesTo', nsmap=WSP_NAMESPACE) endpoint_reference = etree.SubElement( applies_to, '{http://www.w3.org/2005/08/addressing}EndpointReference', nsmap=WSA_NAMESPACE) wsa_address = etree.SubElement( endpoint_reference, '{http://www.w3.org/2005/08/addressing}Address') keytype = etree.SubElement( request_security_token, '{http://docs.oasis-open.org/ws-sx/ws-trust/200512}KeyType') keytype.text = ('http://docs.oasis-open.org/ws-sx/' 'ws-trust/200512/Bearer') request_type = etree.SubElement( request_security_token, '{http://docs.oasis-open.org/ws-sx/ws-trust/200512}RequestType') request_type.text = ('http://docs.oasis-open.org/ws-sx/' 'ws-trust/200512/Issue') token_type = etree.SubElement( request_security_token, '{http://docs.oasis-open.org/ws-sx/ws-trust/200512}TokenType') token_type.text = 'urn:oasis:names:tc:SAML:1.0:assertion' # After constructing the request, let's plug in some values username.text = self.username password.text = self.password to.text = self.identity_provider_url wsa_address.text = self.service_provider_endpoint self.prepared_request = root def _get_adfs_security_token(self, session): """Send ADFS Security token to the ADFS server. Store the result in the instance attribute and raise an exception in case the response is not valid XML data. If a user cannot authenticate due to providing bad credentials, the ADFS2.0 server will return a HTTP 500 response and a XML Fault message. If ``exceptions.InternalServerError`` is caught, the method tries to parse the XML response. If parsing is unsuccessful, an ``exceptions.AuthorizationFailure`` is raised with a reason from the XML fault. Otherwise an original ``exceptions.InternalServerError`` is re-raised. :param session : a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :raises keystoneauth1.exceptions.AuthorizationFailure: when HTTP response from the ADFS server is not a valid XML ADFS security token. :raises keystoneauth1.exceptions.InternalServerError: If response status code is HTTP 500 and the response XML cannot be recognized. """ def _get_failure(e): xpath = '/s:Envelope/s:Body/s:Fault/s:Code/s:Subcode/s:Value' content = e.response.content try: obj = self.str_to_xml(content).xpath( xpath, namespaces=self.NAMESPACES) obj = self._first(obj) return obj.text # NOTE(marek-denis): etree.Element.xpath() doesn't raise an # exception, it just returns an empty list. In that case, _first() # will raise IndexError and we should treat it as an indication XML # is not valid. exceptions.AuthorizationFailure can be raised from # str_to_xml(), however since server returned HTTP 500 we should # re-raise exceptions.InternalServerError. except (IndexError, exceptions.AuthorizationFailure): raise e request_security_token = self.xml_to_str(self.prepared_request) try: response = session.post( url=self.identity_provider_url, headers=self.HEADER_SOAP, data=request_security_token, authenticated=False) except exceptions.InternalServerError as e: reason = _get_failure(e) raise exceptions.AuthorizationFailure(reason) msg = ('Error parsing XML returned from ' 'the ADFS Identity Provider, reason: %s') self.adfs_token = self.str_to_xml(response.content, msg) def _prepare_sp_request(self): """Prepare ADFS Security Token to be sent to the Service Provider. The method works as follows: * Extract SAML2 assertion from the ADFS Security Token. * Replace namespaces * urlencode assertion * concatenate static string with the encoded assertion """ assertion = self.adfs_token.xpath( self.ADFS_ASSERTION_XPATH, namespaces=self.ADFS_TOKEN_NAMESPACES) assertion = self._first(assertion) assertion = self.xml_to_str(assertion) # TODO(marek-denis): Ideally no string replacement should occur. # Unfortunately lxml doesn't allow for namespaces changing in-place and # probably the only solution good for now is to build the assertion # from scratch and reuse values from the adfs security token. assertion = assertion.replace( b'http://docs.oasis-open.org/ws-sx/ws-trust/200512', b'http://schemas.xmlsoap.org/ws/2005/02/trust') encoded_assertion = urllib.parse.quote(assertion) self.encoded_assertion = 'wa=wsignin1.0&wresult=' + encoded_assertion def _send_assertion_to_service_provider(self, session): """Send prepared assertion to a service provider. As the assertion doesn't contain a protected resource, the value from the ``location`` header is not valid and we should not let the Session object get redirected there. The aim of this call is to get a cookie in the response which is required for entering a protected endpoint. :param session : a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :raises: Corresponding HTTP error exception """ session.post( url=self.service_provider_endpoint, data=self.encoded_assertion, headers=self.HEADER_X_FORM, redirect=False, authenticated=False) def _access_service_provider(self, session): """Access protected endpoint and fetch unscoped token. After federated authentication workflow a protected endpoint should be accessible with the session object. The access is granted basing on the cookies stored within the session object. If, for some reason no cookies are present (quantity test) it means something went wrong and user will not be able to fetch an unscoped token. In that case an ``exceptions.AuthorizationFailure` exception is raised and no HTTP call is even made. :param session : a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :raises keystoneauth1.exceptions.AuthorizationFailure: in case session object has empty cookie jar. """ if self._cookies(session) is False: raise exceptions.AuthorizationFailure( "Session object doesn't contain a cookie, therefore you are " "not allowed to enter the Identity Provider's protected area.") self.authenticated_response = session.get(self.federated_token_url, authenticated=False) def get_unscoped_auth_ref(self, session, *kwargs): """Retrieve unscoped token after authentcation with ADFS server. This is a multistep process: * Prepare ADFS Request Securty Token - build an etree.XML object filling certain attributes with proper user credentials, created/expires dates (ticket is be valid for 120 seconds as currently we don't handle reusing ADFS issued security tokens). * Send ADFS Security token to the ADFS server. Step handled by * Receive and parse security token, extract actual SAML assertion and prepare a request addressed for the Service Provider endpoint. This also includes changing namespaces in the XML document. Step handled by ``ADFSPassword._prepare_sp_request()`` method. * Send prepared assertion to the Service Provider endpoint. Usually the server will respond with HTTP 301 code which should be ignored as the 'location' header doesn't contain protected area. The goal of this operation is fetching the session cookie which later allows for accessing protected URL endpoints. Step handed by ``ADFSPassword._send_assertion_to_service_provider()`` method. * Once the session cookie is issued, the protected endpoint can be accessed and an unscoped token can be retrieved. Step handled by ``ADFSPassword._access_service_provider()`` method. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: AccessInfo :rtype: :py:class:`keystoneauth1.access.AccessInfo` """ self._prepare_adfs_request() self._get_adfs_security_token(session) self._prepare_sp_request() self._send_assertion_to_service_provider(session) self._access_service_provider(session) return access.create(resp=self.authenticated_response) keystoneauth-2.4.1/keystoneauth1/extras/_saml2/v3/base.py000066400000000000000000000064121271245473000233540ustar00rootroot00000000000000# 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 lxml import etree from keystoneauth1 import exceptions from keystoneauth1.identity import v3 class _Saml2TokenAuthMethod(v3.AuthMethod): _method_parameters = [] def get_auth_data(self, session, auth, headers, **kwargs): raise exceptions.MethodNotImplemented('This method should never ' 'be called') class BaseSAMLPlugin(v3.FederationBaseAuth): HTTP_MOVED_TEMPORARILY = 302 HTTP_SEE_OTHER = 303 _auth_method_class = _Saml2TokenAuthMethod def __init__(self, auth_url, identity_provider, identity_provider_url, username, password, protocol, **kwargs): """Class constructor accepting following parameters: :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: Name of the Identity Provider the client will authenticate against. This parameter will be used to build a dynamic URL used to obtain unscoped OpenStack token. :type identity_provider: string :param identity_provider_url: An Identity Provider URL, where the SAML2 authn request will be sent. :type identity_provider_url: string :param username: User's login :type username: string :param password: User's password :type password: string :param protocol: Protocol to be used for the authentication. The name must be equal to one configured at the keystone sp side. This value is used for building dynamic authentication URL. Typical value would be: saml2 :type protocol: string """ super(BaseSAMLPlugin, self).__init__( auth_url=auth_url, identity_provider=identity_provider, protocol=protocol, **kwargs) self.identity_provider_url = identity_provider_url self.username = username self.password = password @staticmethod def _first(_list): if len(_list) != 1: raise IndexError('Only single element list is acceptable') return _list[0] @staticmethod def str_to_xml(content, msg=None, include_exc=True): try: return etree.XML(content) except etree.XMLSyntaxError as e: if not msg: msg = str(e) else: msg = msg % e if include_exc else msg raise exceptions.AuthorizationFailure(msg) @staticmethod def xml_to_str(content, **kwargs): return etree.tostring(content, **kwargs) keystoneauth-2.4.1/keystoneauth1/extras/_saml2/v3/saml2.py000066400000000000000000000336011271245473000234600ustar00rootroot00000000000000# 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 lxml import etree from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.extras._saml2.v3 import base class Password(base.BaseSAMLPlugin): """Implement authentication plugin for SAML2 protocol. ECP stands for `Enhanced Client or Proxy` and is a SAML2 extension for federated authentication where a transportation layer consists of HTTP protocol and XML SOAP messages. `Read for more information `_ on ECP. Reference the `SAML2 ECP specification `_. Currently only HTTPBasicAuth mechanism is available for the IdP authenication. :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: name of the Identity Provider the client will authenticate against. This parameter will be used to build a dynamic URL used to obtain unscoped OpenStack token. :type identity_provider: string :param identity_provider_url: An Identity Provider URL, where the SAML2 authn request will be sent. :type identity_provider_url: string :param username: User's login :type username: string :param password: User's password :type password: string :param protocol: Protocol to be used for the authentication. The name must be equal to one configured at the keystone sp side. This value is used for building dynamic authentication URL. Typical value would be: saml2 :type protocol: string """ SAML2_HEADER_INDEX = 0 ECP_SP_EMPTY_REQUEST_HEADERS = { 'Accept': 'text/html, application/vnd.paos+xml', 'PAOS': ('ver="urn:liberty:paos:2003-08";"urn:oasis:names:tc:' 'SAML:2.0:profiles:SSO:ecp"') } ECP_SP_SAML2_REQUEST_HEADERS = { 'Content-Type': 'application/vnd.paos+xml' } ECP_SAML2_NAMESPACES = { 'ecp': 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp', 'S': 'http://schemas.xmlsoap.org/soap/envelope/', 'paos': 'urn:liberty:paos:2003-08' } ECP_RELAY_STATE = '//ecp:RelayState' ECP_SERVICE_PROVIDER_CONSUMER_URL = ('/S:Envelope/S:Header/paos:Request/' '@responseConsumerURL') ECP_IDP_CONSUMER_URL = ('/S:Envelope/S:Header/ecp:Response/' '@AssertionConsumerServiceURL') SOAP_FAULT = """ S:Server responseConsumerURL from SP and assertionConsumerServiceURL from IdP do not match """ def _handle_http_ecp_redirect(self, session, response, method, **kwargs): if response.status_code not in (self.HTTP_MOVED_TEMPORARILY, self.HTTP_SEE_OTHER): return response location = response.headers['location'] return session.request(location, method, authenticated=False, **kwargs) def _prepare_idp_saml2_request(self, saml2_authn_request): header = saml2_authn_request[self.SAML2_HEADER_INDEX] saml2_authn_request.remove(header) def _check_consumer_urls(self, session, sp_response_consumer_url, idp_sp_response_consumer_url): """Check if consumer URLs issued by SP and IdP are equal. In the initial SAML2 authn Request issued by a Service Provider there is a url called ``consumer url``. A trusted Identity Provider should issue identical url. If the URLs are not equal the federated authn process should be interrupted and the user should be warned. :param session: session object to send out HTTP requests. :type session: keystoneauth1.session.Session :param sp_response_consumer_url: consumer URL issued by a SP :type sp_response_consumer_url: string :param idp_sp_response_consumer_url: consumer URL issued by an IdP :type idp_sp_response_consumer_url: string """ if sp_response_consumer_url != idp_sp_response_consumer_url: # send fault message to the SP, discard the response session.post(sp_response_consumer_url, data=self.SOAP_FAULT, headers=self.ECP_SP_SAML2_REQUEST_HEADERS, authenticated=False) # prepare error message and raise an exception. msg = ('Consumer URLs from Service Provider %(service_provider)s ' '%(sp_consumer_url)s and Identity Provider ' '%(identity_provider)s %(idp_consumer_url)s are not equal') msg = msg % { 'service_provider': self.federated_token_url, 'sp_consumer_url': sp_response_consumer_url, 'identity_provider': self.identity_provider, 'idp_consumer_url': idp_sp_response_consumer_url } raise exceptions.AuthorizationFailure(msg) def _send_service_provider_request(self, session): """Initial HTTP GET request to the SAML2 protected endpoint. It's crucial to include HTTP headers indicating that the client is willing to take advantage of the ECP SAML2 extension and receive data as the SOAP. Unlike standard authentication methods in the OpenStack Identity, the client accesses:: ``/v3/OS-FEDERATION/identity_providers/{identity_providers}/ protocols/{protocol}/auth`` After a successful HTTP call the HTTP response should include SAML2 authn request in the XML format. If a HTTP response contains ``X-Subject-Token`` in the headers and the response body is a valid JSON assume the user was already authenticated and Keystone returned a valid unscoped token. Return True indicating the user was already authenticated. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session """ sp_response = session.get(self.federated_token_url, headers=self.ECP_SP_EMPTY_REQUEST_HEADERS, authenticated=False) if 'X-Subject-Token' in sp_response.headers: self.authenticated_response = sp_response return True try: self.saml2_authn_request = etree.XML(sp_response.content) except etree.XMLSyntaxError as e: msg = ('SAML2: Error parsing XML returned ' 'from Service Provider, reason: %s') % e raise exceptions.AuthorizationFailure(msg) relay_state = self.saml2_authn_request.xpath( self.ECP_RELAY_STATE, namespaces=self.ECP_SAML2_NAMESPACES) self.relay_state = self._first(relay_state) sp_response_consumer_url = self.saml2_authn_request.xpath( self.ECP_SERVICE_PROVIDER_CONSUMER_URL, namespaces=self.ECP_SAML2_NAMESPACES) self.sp_response_consumer_url = self._first(sp_response_consumer_url) return False def _send_idp_saml2_authn_request(self, session): """Present modified SAML2 authn assertion from the Service Provider.""" self._prepare_idp_saml2_request(self.saml2_authn_request) idp_saml2_authn_request = self.saml2_authn_request # Currently HTTPBasicAuth method is hardcoded into the plugin idp_response = session.post( self.identity_provider_url, headers={'Content-type': 'text/xml'}, data=etree.tostring(idp_saml2_authn_request), requests_auth=(self.username, self.password), authenticated=False, log=False) try: self.saml2_idp_authn_response = etree.XML(idp_response.content) except etree.XMLSyntaxError as e: msg = ('SAML2: Error parsing XML returned ' 'from Identity Provider, reason: %s') % e raise exceptions.AuthorizationFailure(msg) idp_response_consumer_url = self.saml2_idp_authn_response.xpath( self.ECP_IDP_CONSUMER_URL, namespaces=self.ECP_SAML2_NAMESPACES) self.idp_response_consumer_url = self._first(idp_response_consumer_url) self._check_consumer_urls(session, self.idp_response_consumer_url, self.sp_response_consumer_url) def _send_service_provider_saml2_authn_response(self, session): """Present SAML2 assertion to the Service Provider. The assertion is issued by a trusted Identity Provider for the authenticated user. This function directs the HTTP request to SP managed URL, for instance: ``https://:/Shibboleth.sso/ SAML2/ECP``. Upon success the there's a session created and access to the protected resource is granted. Many implementations of the SP return HTTP 302 status code pointing to the protected URL (``https://:/v3/ OS-FEDERATION/identity_providers/{identity_provider}/protocols/ {protocol_id}/auth`` in this case). Saml2 plugin should point to that URL again, with HTTP GET method, expecting an unscoped token. :param session: a session object to send out HTTP requests. """ self.saml2_idp_authn_response[0][0] = self.relay_state response = session.post( self.idp_response_consumer_url, headers=self.ECP_SP_SAML2_REQUEST_HEADERS, data=etree.tostring(self.saml2_idp_authn_response), authenticated=False, redirect=False) # Don't follow HTTP specs - after the HTTP 302/303 response don't # repeat the call directed to the Location URL. In this case, this is # an indication that saml2 session is now active and protected resource # can be accessed. response = self._handle_http_ecp_redirect( session, response, method='GET', headers=self.ECP_SP_SAML2_REQUEST_HEADERS) self.authenticated_response = response def get_unscoped_auth_ref(self, session): """Get unscoped OpenStack token after federated authentication. This is a multi-step process including multiple HTTP requests. The federated authentication consists of: * HTTP GET request to the Identity Service (acting as a Service Provider). It's crucial to include HTTP headers indicating we are expecting SOAP message in return. Service Provider should respond with such SOAP message. This step is handed by a method ``Saml2Password_send_service_provider_request()``. * HTTP POST request to the external Identity Provider service with ECP extension enabled. The content sent is a header removed SOAP message returned from the Service Provider. It's also worth noting that ECP extension to the SAML2 doesn't define authentication method. The most popular is HttpBasicAuth with just user and password. Other possibilities could be X509 certificates or Kerberos. Upon successful authentication the user should receive a SAML2 assertion. This step is handed by a method ``Saml2Password_send_idp_saml2_authn_request(session)`` * HTTP POST request again to the Service Provider. The body of the request includes SAML2 assertion issued by a trusted Identity Provider. The request should be sent to the Service Provider consumer url specified in the SAML2 assertion. Providing the authentication was successful and both Service Provider and Identity Providers are trusted to each other, the Service Provider will issue an unscoped token with a list of groups the federated user is a member of. This step is handed by a method ``Saml2Password_send_service_provider_saml2_authn_response()`` Unscoped token example:: { "token": { "methods": [ "saml2" ], "user": { "id": "username%40example.com", "name": "username@example.com", "OS-FEDERATION": { "identity_provider": "ACME", "protocol": "saml2", "groups": [ {"id": "abc123"}, {"id": "bcd234"} ] } } } } :param session : a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: AccessInfo :rtype: :py:class:`keystoneauth1.access.AccessInfo` """ saml_authenticated = self._send_service_provider_request(session) if not saml_authenticated: self._send_idp_saml2_authn_request(session) self._send_service_provider_saml2_authn_response(session) return access.create(resp=self.authenticated_response) keystoneauth-2.4.1/keystoneauth1/extras/kerberos.py000066400000000000000000000045001271245473000225450ustar00rootroot00000000000000# 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. """Kerberos authentication plugins. .. warning:: This module requires installation of an extra package (`requests_kerberos`) not installed by default. Without the extra package an import error will occur. The extra package can be installed using:: $ pip install keystoneauth['kerberos'] """ import requests_kerberos from keystoneauth1 import access from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import federation def _requests_auth(): # NOTE(jamielennox): request_kerberos.OPTIONAL allows the plugin to accept # unencrypted error messages where we can't verify the origin of the error # because we aren't authenticated. return requests_kerberos.HTTPKerberosAuth( mutual_authentication=requests_kerberos.OPTIONAL) class KerberosMethod(v3.AuthMethod): _method_parameters = [] def get_auth_data(self, session, auth, headers, request_kwargs, **kwargs): # NOTE(jamielennox): request_kwargs is passed as a kwarg however it is # required and always present when called from keystoneclient. request_kwargs['requests_auth'] = _requests_auth() return 'kerberos', {} class Kerberos(v3.AuthConstructor): _auth_method_class = KerberosMethod class MappedKerberos(federation.FederationBaseAuth): """Authenticate using Kerberos via the keystone federation mechanisms. This uses the OS-FEDERATION extension to gain an unscoped token and then use the standard keystone auth process to scope that to any given project. """ def get_unscoped_auth_ref(self, session, **kwargs): resp = session.get(self.federated_token_url, requests_auth=_requests_auth(), authenticated=False) return access.create(body=resp.json(), resp=resp) keystoneauth-2.4.1/keystoneauth1/fixture/000077500000000000000000000000001271245473000205405ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/fixture/__init__.py000066400000000000000000000027101271245473000226510ustar00rootroot00000000000000# 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. """ The generators in this directory produce keystone compliant structures for use in testing. They should be considered part of the public API because they may be relied upon to generate test tokens for other clients. However they should never be imported into the main client (keystonauth or other). Because of this there may be dependencies from this module on libraries that are only available in testing. """ from keystoneauth1.fixture.discovery import * # noqa from keystoneauth1.fixture import exception from keystoneauth1.fixture import v2 from keystoneauth1.fixture import v3 FixtureValidationError = exception.FixtureValidationError V2Token = v2.Token V3Token = v3.Token V3FederationToken = v3.V3FederationToken __all__ = ('DiscoveryList', 'FixtureValidationError', 'V2Discovery', 'V3Discovery', 'V2Token', 'V3Token', 'V3FederationToken', ) keystoneauth-2.4.1/keystoneauth1/fixture/discovery.py000066400000000000000000000200231271245473000231160ustar00rootroot00000000000000# 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 positional import positional from keystoneauth1 import _utils as utils __all__ = ('DiscoveryList', 'V2Discovery', 'V3Discovery', ) _DEFAULT_DAYS_AGO = 30 class DiscoveryBase(dict): """The basic version discovery structure. All version discovery elements should have access to these values. :param string id: The version id for this version entry. :param string status: The status of this entry. :param DateTime updated: When the API was last updated. """ @positional() def __init__(self, id, status=None, updated=None): super(DiscoveryBase, self).__init__() self.id = id self.status = status or 'stable' self.updated = updated or utils.before_utcnow(days=_DEFAULT_DAYS_AGO) @property def id(self): return self.get('id') @id.setter def id(self, value): self['id'] = value @property def status(self): return self.get('status') @status.setter def status(self, value): self['status'] = value @property def links(self): return self.setdefault('links', []) @property def updated_str(self): return self.get('updated') @updated_str.setter def updated_str(self, value): self['updated'] = value @property def updated(self): return utils.parse_isotime(self.updated_str) @updated.setter def updated(self, value): self.updated_str = value.isoformat() @positional() def add_link(self, href, rel='self', type=None): link = {'href': href, 'rel': rel} if type: link['type'] = type self.links.append(link) return link @property def media_types(self): return self.setdefault('media-types', []) @positional(1) def add_media_type(self, base, type): mt = {'base': base, 'type': type} self.media_types.append(mt) return mt class V2Discovery(DiscoveryBase): """A Version element for a V2 identity service endpoint. Provides some default values and helper methods for creating a v2.0 endpoint version structure. Clients should use this instead of creating their own structures. :param string href: The url that this entry should point to. :param string id: The version id that should be reported. (optional) Defaults to 'v2.0'. :param bool html: Add HTML describedby links to the structure. :param bool pdf: Add PDF describedby links to the structure. """ _DESC_URL = 'http://docs.openstack.org/api/openstack-identity-service/2.0/' @positional() def __init__(self, href, id=None, html=True, pdf=True, **kwargs): super(V2Discovery, self).__init__(id or 'v2.0', **kwargs) self.add_link(href) if html: self.add_html_description() if pdf: self.add_pdf_description() def add_html_description(self): """Add the HTML described by links. The standard structure includes a link to a HTML document with the API specification. Add it to this entry. """ self.add_link(href=self._DESC_URL + 'content', rel='describedby', type='text/html') def add_pdf_description(self): """Add the PDF described by links. The standard structure includes a link to a PDF document with the API specification. Add it to this entry. """ self.add_link(href=self._DESC_URL + 'identity-dev-guide-2.0.pdf', rel='describedby', type='application/pdf') class V3Discovery(DiscoveryBase): """A Version element for a V3 identity service endpoint. Provides some default values and helper methods for creating a v3 endpoint version structure. Clients should use this instead of creating their own structures. :param href: The url that this entry should point to. :param string id: The version id that should be reported. (optional) Defaults to 'v3.0'. :param bool json: Add JSON media-type elements to the structure. :param bool xml: Add XML media-type elements to the structure. """ @positional() def __init__(self, href, id=None, json=True, xml=True, **kwargs): super(V3Discovery, self).__init__(id or 'v3.0', **kwargs) self.add_link(href) if json: self.add_json_media_type() if xml: self.add_xml_media_type() def add_json_media_type(self): """Add the JSON media-type links. The standard structure includes a list of media-types that the endpoint supports. Add JSON to the list. """ self.add_media_type(base='application/json', type='application/vnd.openstack.identity-v3+json') def add_xml_media_type(self): """Add the XML media-type links. The standard structure includes a list of media-types that the endpoint supports. Add XML to the list. """ self.add_media_type(base='application/xml', type='application/vnd.openstack.identity-v3+xml') class DiscoveryList(dict): """A List of version elements. Creates a correctly structured list of identity service endpoints for use in testing with discovery. :param string href: The url that this should be based at. :param bool v2: Add a v2 element. :param bool v3: Add a v3 element. :param string v2_status: The status to use for the v2 element. :param DateTime v2_updated: The update time to use for the v2 element. :param bool v2_html: True to add a html link to the v2 element. :param bool v2_pdf: True to add a pdf link to the v2 element. :param string v3_status: The status to use for the v3 element. :param DateTime v3_updated: The update time to use for the v3 element. :param bool v3_json: True to add a html link to the v2 element. :param bool v3_xml: True to add a pdf link to the v2 element. """ TEST_URL = 'http://keystone.host:5000/' @positional(2) def __init__(self, href=None, v2=True, v3=True, v2_id=None, v3_id=None, v2_status=None, v2_updated=None, v2_html=True, v2_pdf=True, v3_status=None, v3_updated=None, v3_json=True, v3_xml=True): super(DiscoveryList, self).__init__(versions={'values': []}) href = href or self.TEST_URL if v2: v2_href = href.rstrip('/') + '/v2.0' self.add_v2(v2_href, id=v2_id, status=v2_status, updated=v2_updated, html=v2_html, pdf=v2_pdf) if v3: v3_href = href.rstrip('/') + '/v3' self.add_v3(v3_href, id=v3_id, status=v3_status, updated=v3_updated, json=v3_json, xml=v3_xml) @property def versions(self): return self['versions']['values'] def add_version(self, version): """Add a new version structure to the list. :param dict version: A new version structure to add to the list. """ self.versions.append(version) def add_v2(self, href, **kwargs): """Add a v2 version to the list. The parameters are the same as V2Discovery. """ obj = V2Discovery(href, **kwargs) self.add_version(obj) return obj def add_v3(self, href, **kwargs): """Add a v3 version to the list. The parameters are the same as V3Discovery. """ obj = V3Discovery(href, **kwargs) self.add_version(obj) return obj keystoneauth-2.4.1/keystoneauth1/fixture/exception.py000066400000000000000000000014651271245473000231160ustar00rootroot00000000000000# 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 FixtureValidationError(Exception): """The token you created is not legitimate. The data contained in the token that was generated is not valid and would not have been returned from a keystone server. You should not do testing with this token. """ keystoneauth-2.4.1/keystoneauth1/fixture/keystoneauth_betamax.py000066400000000000000000000047701271245473000253460ustar00rootroot00000000000000# 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. """A fixture to wrap the session constructor for use with Betamax""" from functools import partial import betamax import fixtures import mock import requests from keystoneauth1 import session class BetamaxFixture(fixtures.Fixture): def __init__(self, cassette_name, cassette_library_dir=None, serializer=None, record=False): self.cassette_library_dir = cassette_library_dir self.serializer = serializer self.record = record self.cassette_name = cassette_name if serializer: betamax.Betamax.register_serializer(serializer) def setUp(self): super(BetamaxFixture, self).setUp() self.mockpatch = mock.patch.object( session, '_construct_session', partial(_construct_session_with_betamax, self)) self.mockpatch.start() # Unpatch during cleanup self.addCleanup(self.mockpatch.stop) def _construct_session_with_betamax(fixture, session_obj=None): # NOTE(morganfainberg): This function should contain the logic of # keystoneauth1.session._construct_session as it replaces the # _construct_session function to apply betamax magic to the requests # session object. if not session_obj: session_obj = requests.Session() # Use TCPKeepAliveAdapter to fix bug 1323862 for scheme in list(session_obj.adapters.keys()): session_obj.mount(scheme, session.TCPKeepAliveAdapter()) fixture.recorder = betamax.Betamax( session_obj, cassette_library_dir=fixture.cassette_library_dir) record = 'none' serializer = None if fixture.record: record = 'all' if fixture.serializer: serializer = fixture.serializer.name fixture.recorder.use_cassette(fixture.cassette_name, serialize_with=serializer, record=record) fixture.recorder.start() fixture.addCleanup(fixture.recorder.stop) return session_obj keystoneauth-2.4.1/keystoneauth1/fixture/v2.py000066400000000000000000000165501271245473000214500ustar00rootroot00000000000000# 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 uuid from keystoneauth1 import _utils from keystoneauth1.fixture import exception class _Service(dict): def add_endpoint(self, public, admin=None, internal=None, tenant_id=None, region=None, id=None): data = {'tenantId': tenant_id or uuid.uuid4().hex, 'publicURL': public, 'adminURL': admin or public, 'internalURL': internal or public, 'region': region, 'id': id or uuid.uuid4().hex} self.setdefault('endpoints', []).append(data) return data class Token(dict): """A V2 Keystone token that can be used for testing. This object is designed to allow clients to generate a correct V2 token for use in there test code. It should prevent clients from having to know the correct token format and allow them to test the portions of token handling that matter to them and not copy and paste sample. """ def __init__(self, token_id=None, expires=None, issued=None, tenant_id=None, tenant_name=None, user_id=None, user_name=None, trust_id=None, trustee_user_id=None, audit_id=None, audit_chain_id=None): super(Token, self).__init__() self.token_id = token_id or uuid.uuid4().hex self.user_id = user_id or uuid.uuid4().hex self.user_name = user_name or uuid.uuid4().hex self.audit_id = audit_id or uuid.uuid4().hex if not issued: issued = _utils.before_utcnow(minutes=2) if not expires: expires = issued + datetime.timedelta(hours=1) try: self.issued = issued except (TypeError, AttributeError): # issued should be able to be passed as a string so ignore self.issued_str = issued try: self.expires = expires except (TypeError, AttributeError): # expires should be able to be passed as a string so ignore self.expires_str = expires if tenant_id or tenant_name: self.set_scope(tenant_id, tenant_name) if trust_id or trustee_user_id: # the trustee_user_id will generally be the same as the user_id as # the token is being issued to the trustee self.set_trust(id=trust_id, trustee_user_id=trustee_user_id or user_id) if audit_chain_id: self.audit_chain_id = audit_chain_id @property def root(self): return self.setdefault('access', {}) @property def _token(self): return self.root.setdefault('token', {}) @property def token_id(self): return self._token['id'] @token_id.setter def token_id(self, value): self._token['id'] = value @property def expires_str(self): return self._token['expires'] @expires_str.setter def expires_str(self, value): self._token['expires'] = value @property def expires(self): return _utils.parse_isotime(self.expires_str) @expires.setter def expires(self, value): self.expires_str = value.isoformat() @property def issued_str(self): return self._token['issued_at'] @issued_str.setter def issued_str(self, value): self._token['issued_at'] = value @property def issued(self): return _utils.parse_isotime(self.issued_str) @issued.setter def issued(self, value): self.issued_str = value.isoformat() @property def _user(self): return self.root.setdefault('user', {}) @property def user_id(self): return self._user['id'] @user_id.setter def user_id(self, value): self._user['id'] = value @property def user_name(self): return self._user['name'] @user_name.setter def user_name(self, value): self._user['name'] = value @property def tenant_id(self): return self._token.get('tenant', {}).get('id') @tenant_id.setter def tenant_id(self, value): self._token.setdefault('tenant', {})['id'] = value @property def tenant_name(self): return self._token.get('tenant', {}).get('name') @tenant_name.setter def tenant_name(self, value): self._token.setdefault('tenant', {})['name'] = value @property def _metadata(self): return self.root.setdefault('metadata', {}) @property def trust_id(self): return self.root.setdefault('trust', {}).get('id') @trust_id.setter def trust_id(self, value): self.root.setdefault('trust', {})['id'] = value @property def trustee_user_id(self): return self.root.setdefault('trust', {}).get('trustee_user_id') @trustee_user_id.setter def trustee_user_id(self, value): self.root.setdefault('trust', {})['trustee_user_id'] = value @property def audit_id(self): try: return self._token.get('audit_ids', [])[0] except IndexError: return None @audit_id.setter def audit_id(self, value): audit_chain_id = self.audit_chain_id lval = [value] if audit_chain_id else [value, audit_chain_id] self._token['audit_ids'] = lval @property def audit_chain_id(self): try: return self._token.get('audit_ids', [])[1] except IndexError: return None @audit_chain_id.setter def audit_chain_id(self, value): self._token['audit_ids'] = [self.audit_id, value] def validate(self): scoped = 'tenant' in self.token catalog = self.root.get('serviceCatalog') if catalog and not scoped: msg = 'You cannot have a service catalog on an unscoped token' raise exception.FixtureValidationError(msg) if scoped and not self.user.get('roles'): msg = 'You must have roles on a token to scope it' raise exception.FixtureValidationError(msg) def add_role(self, name=None, id=None): id = id or uuid.uuid4().hex name = name or uuid.uuid4().hex roles = self._user.setdefault('roles', []) roles.append({'name': name}) self._metadata.setdefault('roles', []).append(id) return {'id': id, 'name': name} def add_service(self, type, name=None): name = name or uuid.uuid4().hex service = _Service(name=name, type=type) self.root.setdefault('serviceCatalog', []).append(service) return service def set_scope(self, id=None, name=None): self.tenant_id = id or uuid.uuid4().hex self.tenant_name = name or uuid.uuid4().hex def set_trust(self, id=None, trustee_user_id=None): self.trust_id = id or uuid.uuid4().hex self.trustee_user_id = trustee_user_id or uuid.uuid4().hex def set_bind(self, name, data): self._token.setdefault('bind', {})[name] = data keystoneauth-2.4.1/keystoneauth1/fixture/v3.py000066400000000000000000000344101271245473000214440ustar00rootroot00000000000000# 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 uuid from keystoneauth1 import _utils from keystoneauth1.fixture import exception class _Service(dict): """One of the services that exist in the catalog. You use this by adding a service to a token which returns an instance of this object and then you can add_endpoints to the service. """ def add_endpoint(self, interface, url, region=None, id=None): data = {'id': id or uuid.uuid4().hex, 'interface': interface, 'url': url, 'region': region, 'region_id': region} self.setdefault('endpoints', []).append(data) return data def add_standard_endpoints(self, public=None, admin=None, internal=None, region=None): ret = [] if public: ret.append(self.add_endpoint('public', public, region=region)) if admin: ret.append(self.add_endpoint('admin', admin, region=region)) if internal: ret.append(self.add_endpoint('internal', internal, region=region)) return ret class Token(dict): """A V3 Keystone token that can be used for testing. This object is designed to allow clients to generate a correct V3 token for use in there test code. It should prevent clients from having to know the correct token format and allow them to test the portions of token handling that matter to them and not copy and paste sample. """ def __init__(self, expires=None, issued=None, user_id=None, user_name=None, user_domain_id=None, user_domain_name=None, methods=None, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None, domain_id=None, domain_name=None, trust_id=None, trust_impersonation=None, trustee_user_id=None, trustor_user_id=None, oauth_access_token_id=None, oauth_consumer_id=None, audit_id=None, audit_chain_id=None): super(Token, self).__init__() self.user_id = user_id or uuid.uuid4().hex self.user_name = user_name or uuid.uuid4().hex self.user_domain_id = user_domain_id or uuid.uuid4().hex self.user_domain_name = user_domain_name or uuid.uuid4().hex self.audit_id = audit_id or uuid.uuid4().hex if not methods: methods = ['password'] self.methods.extend(methods) if not issued: issued = _utils.before_utcnow(minutes=2) try: self.issued = issued except (TypeError, AttributeError): # issued should be able to be passed as a string so ignore self.issued_str = issued if not expires: expires = self.issued + datetime.timedelta(hours=1) try: self.expires = expires except (TypeError, AttributeError): # expires should be able to be passed as a string so ignore self.expires_str = expires if (project_id or project_name or project_domain_id or project_domain_name): self.set_project_scope(id=project_id, name=project_name, domain_id=project_domain_id, domain_name=project_domain_name) if domain_id or domain_name: self.set_domain_scope(id=domain_id, name=domain_name) if (trust_id or (trust_impersonation is not None) or trustee_user_id or trustor_user_id): self.set_trust_scope(id=trust_id, impersonation=trust_impersonation, trustee_user_id=trustee_user_id, trustor_user_id=trustor_user_id) if oauth_access_token_id or oauth_consumer_id: self.set_oauth(access_token_id=oauth_access_token_id, consumer_id=oauth_consumer_id) if audit_chain_id: self.audit_chain_id = audit_chain_id @property def root(self): return self.setdefault('token', {}) @property def expires_str(self): return self.root.get('expires_at') @expires_str.setter def expires_str(self, value): self.root['expires_at'] = value @property def expires(self): return _utils.parse_isotime(self.expires_str) @expires.setter def expires(self, value): self.expires_str = value.isoformat() @property def issued_str(self): return self.root.get('issued_at') @issued_str.setter def issued_str(self, value): self.root['issued_at'] = value @property def issued(self): return _utils.parse_isotime(self.issued_str) @issued.setter def issued(self, value): self.issued_str = value.isoformat() @property def _user(self): return self.root.setdefault('user', {}) @property def user_id(self): return self._user.get('id') @user_id.setter def user_id(self, value): self._user['id'] = value @property def user_name(self): return self._user.get('name') @user_name.setter def user_name(self, value): self._user['name'] = value @property def _user_domain(self): return self._user.setdefault('domain', {}) @_user_domain.setter def _user_domain(self, domain): self._user['domain'] = domain @property def user_domain_id(self): return self._user_domain.get('id') @user_domain_id.setter def user_domain_id(self, value): self._user_domain['id'] = value @property def user_domain_name(self): return self._user_domain.get('name') @user_domain_name.setter def user_domain_name(self, value): self._user_domain['name'] = value @property def methods(self): return self.root.setdefault('methods', []) @property def project_id(self): return self.root.get('project', {}).get('id') @project_id.setter def project_id(self, value): self.root.setdefault('project', {})['id'] = value @property def project_name(self): return self.root.get('project', {}).get('name') @project_name.setter def project_name(self, value): self.root.setdefault('project', {})['name'] = value @property def project_domain_id(self): return self.root.get('project', {}).get('domain', {}).get('id') @project_domain_id.setter def project_domain_id(self, value): project = self.root.setdefault('project', {}) project.setdefault('domain', {})['id'] = value @property def project_domain_name(self): return self.root.get('project', {}).get('domain', {}).get('name') @project_domain_name.setter def project_domain_name(self, value): project = self.root.setdefault('project', {}) project.setdefault('domain', {})['name'] = value @property def domain_id(self): return self.root.get('domain', {}).get('id') @domain_id.setter def domain_id(self, value): self.root.setdefault('domain', {})['id'] = value @property def domain_name(self): return self.root.get('domain', {}).get('name') @domain_name.setter def domain_name(self, value): self.root.setdefault('domain', {})['name'] = value @property def trust_id(self): return self.root.get('OS-TRUST:trust', {}).get('id') @trust_id.setter def trust_id(self, value): self.root.setdefault('OS-TRUST:trust', {})['id'] = value @property def trust_impersonation(self): return self.root.get('OS-TRUST:trust', {}).get('impersonation') @trust_impersonation.setter def trust_impersonation(self, value): self.root.setdefault('OS-TRUST:trust', {})['impersonation'] = value @property def trustee_user_id(self): trust = self.root.get('OS-TRUST:trust', {}) return trust.get('trustee_user', {}).get('id') @trustee_user_id.setter def trustee_user_id(self, value): trust = self.root.setdefault('OS-TRUST:trust', {}) trust.setdefault('trustee_user', {})['id'] = value @property def trustor_user_id(self): trust = self.root.get('OS-TRUST:trust', {}) return trust.get('trustor_user', {}).get('id') @trustor_user_id.setter def trustor_user_id(self, value): trust = self.root.setdefault('OS-TRUST:trust', {}) trust.setdefault('trustor_user', {})['id'] = value @property def oauth_access_token_id(self): return self.root.get('OS-OAUTH1', {}).get('access_token_id') @oauth_access_token_id.setter def oauth_access_token_id(self, value): self.root.setdefault('OS-OAUTH1', {})['access_token_id'] = value @property def oauth_consumer_id(self): return self.root.get('OS-OAUTH1', {}).get('consumer_id') @oauth_consumer_id.setter def oauth_consumer_id(self, value): self.root.setdefault('OS-OAUTH1', {})['consumer_id'] = value @property def audit_id(self): try: return self.root.get('audit_ids', [])[0] except IndexError: return None @audit_id.setter def audit_id(self, value): audit_chain_id = self.audit_chain_id lval = [value] if audit_chain_id else [value, audit_chain_id] self.root['audit_ids'] = lval @property def audit_chain_id(self): try: return self.root.get('audit_ids', [])[1] except IndexError: return None @audit_chain_id.setter def audit_chain_id(self, value): self.root['audit_ids'] = [self.audit_id, value] @property def role_ids(self): return [r['id'] for r in self.root.get('roles', [])] @property def role_names(self): return [r['name'] for r in self.root.get('roles', [])] def validate(self): project = self.root.get('project') domain = self.root.get('domain') trust = self.root.get('OS-TRUST:trust') catalog = self.root.get('catalog') roles = self.root.get('roles') scoped = project or domain or trust if sum((bool(project), bool(domain), bool(trust))) > 1: msg = 'You cannot scope to multiple targets' raise exception.FixtureValidationError(msg) if catalog and not scoped: msg = 'You cannot have a service catalog on an unscoped token' raise exception.FixtureValidationError(msg) if scoped and not self.user.get('roles'): msg = 'You must have roles on a token to scope it' raise exception.FixtureValidationError(msg) if bool(scoped) != bool(roles): msg = 'You must be scoped to have roles and vice-versa' raise exception.FixtureValidationError(msg) def add_role(self, name=None, id=None): roles = self.root.setdefault('roles', []) data = {'id': id or uuid.uuid4().hex, 'name': name or uuid.uuid4().hex} roles.append(data) return data def add_service(self, type, name=None, id=None): service = _Service(type=type, id=id or uuid.uuid4().hex) if name: service['name'] = name self.root.setdefault('catalog', []).append(service) return service def set_project_scope(self, id=None, name=None, domain_id=None, domain_name=None): self.project_id = id or uuid.uuid4().hex self.project_name = name or uuid.uuid4().hex self.project_domain_id = domain_id or uuid.uuid4().hex self.project_domain_name = domain_name or uuid.uuid4().hex def set_domain_scope(self, id=None, name=None): self.domain_id = id or uuid.uuid4().hex self.domain_name = name or uuid.uuid4().hex def set_trust_scope(self, id=None, impersonation=False, trustee_user_id=None, trustor_user_id=None): self.trust_id = id or uuid.uuid4().hex self.trust_impersonation = impersonation self.trustee_user_id = trustee_user_id or uuid.uuid4().hex self.trustor_user_id = trustor_user_id or uuid.uuid4().hex def set_oauth(self, access_token_id=None, consumer_id=None): self.oauth_access_token_id = access_token_id or uuid.uuid4().hex self.oauth_consumer_id = consumer_id or uuid.uuid4().hex @property def service_providers(self): return self.root.get('service_providers') def add_service_provider(self, sp_id, sp_auth_url, sp_url): _service_providers = self.root.setdefault('service_providers', []) sp = {'id': sp_id, 'auth_url': sp_auth_url, 'sp_url': sp_url} _service_providers.append(sp) return sp def set_bind(self, name, data): self.root.setdefault('bind', {})[name] = data class V3FederationToken(Token): """A V3 Keystone Federation token that can be used for testing. Similar to V3Token, this object is designed to allow clients to generate a correct V3 federation token for use in test code. """ FEDERATED_DOMAIN_ID = 'Federated' def __init__(self, methods=None, identity_provider=None, protocol=None, groups=None): methods = methods or ['saml2'] super(V3FederationToken, self).__init__(methods=methods) self._user_domain = {'id': V3FederationToken.FEDERATED_DOMAIN_ID} self.add_federation_info_to_user(identity_provider, protocol, groups) def add_federation_info_to_user(self, identity_provider=None, protocol=None, groups=None): data = { "OS-FEDERATION": { "identity_provider": identity_provider or uuid.uuid4().hex, "protocol": protocol or uuid.uuid4().hex, "groups": groups or [{"id": uuid.uuid4().hex}] } } self._user.update(data) return data keystoneauth-2.4.1/keystoneauth1/hacking/000077500000000000000000000000001271245473000204565ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/hacking/__init__.py000066400000000000000000000000001271245473000225550ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/hacking/checks.py000066400000000000000000000023471271245473000222760ustar00rootroot00000000000000# 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. """keystoneauth1's pep8 extensions. In order to make the review process faster and easier for core devs we are adding some keystoneauth1 specific pep8 checks. This will catch common errors so that core devs don't have to. """ import re def check_oslo_namespace_imports(logical_line, blank_before, filename): oslo_namespace_imports = re.compile( r"(((from)|(import))\s+oslo\.)|(from\s+oslo\s+import\s+)") if re.match(oslo_namespace_imports, logical_line): msg = ("K333: '%s' must be used instead of '%s'.") % ( logical_line.replace('oslo.', 'oslo_'), logical_line) yield(0, msg) def factory(register): register(check_oslo_namespace_imports) keystoneauth-2.4.1/keystoneauth1/identity/000077500000000000000000000000001271245473000207035ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/identity/__init__.py000066400000000000000000000023571271245473000230230ustar00rootroot00000000000000# 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 keystoneauth1.identity import base from keystoneauth1.identity import generic from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import oidc BaseIdentityPlugin = base.BaseIdentityPlugin V2Password = v2.Password V2Token = v2.Token V3Password = v3.Password V3Token = v3.Token Password = generic.Password Token = generic.Token V3OidcPassword = oidc.OidcPassword V3OidcAuthorizationCode = oidc.OidcAuthorizationCode __all__ = ('BaseIdentityPlugin', 'Password', 'Token', 'V2Password', 'V2Token', 'V3Password', 'V3Token', 'V3OidcPassword', 'V3OidcAuthorizationCode') keystoneauth-2.4.1/keystoneauth1/identity/access.py000066400000000000000000000035571271245473000225300ustar00rootroot00000000000000# 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 positional import positional from keystoneauth1.identity import base class AccessInfoPlugin(base.BaseIdentityPlugin): """A plugin that turns an existing AccessInfo object into a usable plugin. There are cases where reuse of an auth_ref or AccessInfo object is warranted such as from a cache, from auth_token middleware, or another source. Turn the existing access info object into an identity plugin. This plugin cannot be refreshed as the AccessInfo object does not contain any authorizing information. :param auth_ref: the existing AccessInfo object. :type auth_ref: keystonauth.access.AccessInfo :param auth_url: the url where this AccessInfo was retrieved from. Required if using the AUTH_INTERFACE with get_endpoint. (optional) """ @positional() def __init__(self, auth_ref, auth_url=None): super(AccessInfoPlugin, self).__init__(auth_url=auth_url, reauthenticate=False) self.auth_ref = auth_ref def get_auth_ref(self, session, **kwargs): return self.auth_ref def invalidate(self): # NOTE(jamielennox): Don't allow the default invalidation to occur # because on next authentication request we will only get the same # auth_ref object again. return False keystoneauth-2.4.1/keystoneauth1/identity/base.py000066400000000000000000000367751271245473000222110ustar00rootroot00000000000000# 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 abc import base64 import hashlib import json import threading from positional import positional import six from keystoneauth1 import _utils as utils from keystoneauth1 import access from keystoneauth1 import discover from keystoneauth1 import exceptions from keystoneauth1 import plugin LOG = utils.get_logger(__name__) @six.add_metaclass(abc.ABCMeta) class BaseIdentityPlugin(plugin.BaseAuthPlugin): # we count a token as valid (not needing refreshing) if it is valid for at # least this many seconds before the token expiry time MIN_TOKEN_LIFE_SECONDS = 120 def __init__(self, auth_url=None, reauthenticate=True): super(BaseIdentityPlugin, self).__init__() self.auth_url = auth_url self.auth_ref = None self.reauthenticate = reauthenticate self._endpoint_cache = {} self._lock = threading.Lock() @abc.abstractmethod def get_auth_ref(self, session, **kwargs): """Obtain a token from an OpenStack Identity Service. This method is overridden by the various token version plugins. This function should not be called independently and is expected to be invoked via the do_authenticate function. This function will be invoked if the AcessInfo object cached by the plugin is not valid. Thus plugins should always fetch a new AccessInfo when invoked. If you are looking to just retrieve the current auth data then you should use get_access. :param session: A session object that can be used for communication. :type session: keystonauth.session.Session :raises keystonauth.exceptions.InvalidResponse: The response returned wasn't appropriate. :raises keystonauth.exceptions.HttpError: An error from an invalid HTTP response. :returns: Token access information. :rtype: :py:class:`keystonauth.access.AccessInfo` """ def get_token(self, session, **kwargs): """Return a valid auth token. If a valid token is not present then a new one will be fetched. :param session: A session object that can be used for communication. :type session: keystonauth.session.Session :raises keystonauth.exceptions.HttpError: An error from an invalid HTTP response. :return: A valid token. :rtype: string """ return self.get_access(session).auth_token def _needs_reauthenticate(self): """Return if the existing token needs to be re-authenticated. The token should be refreshed if it is about to expire. :returns: True if the plugin should fetch a new token. False otherwise. """ if not self.auth_ref: # authentication was never fetched. return True if not self.reauthenticate: # don't re-authenticate if it has been disallowed. return False if self.auth_ref.will_expire_soon(self.MIN_TOKEN_LIFE_SECONDS): # if it's about to expire we should re-authenticate now. return True # otherwise it's fine and use the existing one. return False def get_access(self, session, **kwargs): """Fetch or return a current AccessInfo object. If a valid AccessInfo is present then it is returned otherwise a new one will be fetched. :param session: A session object that can be used for communication. :type session: keystonauth.session.Session :raises keystonauth.exceptions.HttpError: An error from an invalid HTTP response. :returns: Valid AccessInfo :rtype: :py:class:`keystonauth.access.AccessInfo` """ # Hey Kids! Thread safety is important particularly in the case where # a service is creating an admin style plugin that will then proceed # to make calls from many threads. As a token expires all the threads # will try and fetch a new token at once, so we want to ensure that # only one thread tries to actually fetch from keystone at once. with self._lock: if self._needs_reauthenticate(): self.auth_ref = self.get_auth_ref(session) return self.auth_ref def invalidate(self): """Invalidate the current authentication data. This should result in fetching a new token on next call. A plugin may be invalidated if an Unauthorized HTTP response is returned to indicate that the token may have been revoked or is otherwise now invalid. :returns: True if there was something that the plugin did to invalidate. This means that it makes sense to try again. If nothing happens returns False to indicate give up. :rtype: bool """ if self.auth_ref: self.auth_ref = None return True return False def get_endpoint(self, session, service_type=None, interface=None, region_name=None, service_name=None, version=None, **kwargs): """Return a valid endpoint for a service. If a valid token is not present then a new one will be fetched using the session and kwargs. :param session: A session object that can be used for communication. :type session: keystonauth.session.Session :param string service_type: The type of service to lookup the endpoint for. This plugin will return None (failure) if service_type is not provided. :param string interface: The exposure of the endpoint. Should be `public`, `internal`, `admin`, or `auth`. `auth` is special here to use the `auth_url` rather than a URL extracted from the service catalog. Defaults to `public`. :param string region_name: The region the endpoint should exist in. (optional) :param string service_name: The name of the service in the catalog. (optional) :param tuple version: The minimum version number required for this endpoint. (optional) :raises keystonauth.exceptions.HttpError: An error from an invalid HTTP response. :return: A valid endpoint URL or None if not available. :rtype: string or None """ # NOTE(jamielennox): if you specifically ask for requests to be sent to # the auth url then we can ignore many of the checks. Typically if you # are asking for the auth endpoint it means that there is no catalog to # query however we still need to support asking for a specific version # of the auth_url for generic plugins. if interface is plugin.AUTH_INTERFACE: url = self.auth_url service_type = service_type or 'identity' else: if not service_type: LOG.warning('Plugin cannot return an endpoint without ' 'knowing the service type that is required. Add ' 'service_type to endpoint filtering data.') return None if not interface: interface = 'public' service_catalog = self.get_access(session).service_catalog url = service_catalog.url_for(service_type=service_type, interface=interface, region_name=region_name, service_name=service_name) if not version: # NOTE(jamielennox): This may not be the best thing to default to # but is here for backwards compatibility. It may be worth # defaulting to the most recent version. return url # NOTE(jamielennox): For backwards compatibility people might have a # versioned endpoint in their catalog even though they want to use # other endpoint versions. So we support a list of client defined # situations where we can strip the version component from a URL before # doing discovery. hacked_url = discover._get_catalog_discover_hack(service_type, url) try: disc = self.get_discovery(session, hacked_url, authenticated=False) except (exceptions.DiscoveryFailure, exceptions.HttpError, exceptions.ConnectionError): # NOTE(jamielennox): Again if we can't contact the server we fall # back to just returning the URL from the catalog. This may not be # the best default but we need it for now. LOG.warning('Failed to contact the endpoint at %s for discovery. ' 'Fallback to using that endpoint as the base url.', url) else: url = disc.url_for(version) return url def get_user_id(self, session, **kwargs): return self.get_access(session).user_id def get_project_id(self, session, **kwargs): return self.get_access(session).project_id def get_sp_auth_url(self, session, sp_id, **kwargs): try: return self.get_access( session).service_providers.get_auth_url(sp_id) except exceptions.ServiceProviderNotFound: return None def get_sp_url(self, session, sp_id, **kwargs): try: return self.get_access( session).service_providers.get_sp_url(sp_id) except exceptions.ServiceProviderNotFound: return None @positional() def get_discovery(self, session, url, authenticated=None): """Return the discovery object for a URL. Check the session and the plugin cache to see if we have already performed discovery on the URL and if so return it, otherwise create a new discovery object, cache it and return it. This function is expected to be used by subclasses and should not be needed by users. :param session: A session object to discover with. :type session: keystonauth.session.Session :param str url: The url to lookup. :param bool authenticated: Include a token in the discovery call. (optional) Defaults to None (use a token if a plugin is installed). :raises keystonauth.exceptions.DiscoveryFailure: if for some reason the lookup fails. :raises keystonauth.exceptions.HttpError: An error from an invalid HTTP response. :returns: A discovery object with the results of looking up that URL. """ # NOTE(jamielennox): we want to cache endpoints on the session as well # so that they maintain sharing between auth plugins. Create a cache on # the session if it doesn't exist already. try: session_endpoint_cache = session._identity_endpoint_cache except AttributeError: session_endpoint_cache = session._identity_endpoint_cache = {} # NOTE(jamielennox): There is a cache located on both the session # object and the auth plugin object so that they can be shared and the # cache is still usable for cache in (self._endpoint_cache, session_endpoint_cache): disc = cache.get(url) if disc: break else: disc = discover.Discover(session, url, authenticated=authenticated) self._endpoint_cache[url] = disc session_endpoint_cache[url] = disc return disc def get_cache_id_elements(self): """Get the elements for this auth plugin that make it unique. As part of the get_cache_id requirement we need to determine what aspects of this plugin and its values that make up the unique elements. This should be overriden by plugins that wish to allow caching. :returns: The unique attributes and values of this plugin. :rtype: A flat dict with a str key and str or None value. This is required as we feed these values into a hash. Pairs where the value is None are ignored in the hashed id. """ raise NotImplementedError() def get_cache_id(self): """Fetch an identifier that uniquely identifies the auth options. The returned identifier need not be decomposable or otherwise provide any way to recreate the plugin. This string MUST change if any of the parameters that are used to uniquely identity this plugin change. It should not change upon a reauthentication of the plugin. :returns: A unique string for the set of options :rtype: str or None if this is unsupported or unavailable. """ try: elements = self.get_cache_id_elements() except NotImplementedError: return None hasher = hashlib.sha256() for k, v in sorted(six.iteritems(elements)): if v is not None: # NOTE(jamielennox): in python3 you need to pass bytes to hash if isinstance(k, six.string_types): k = k.encode('utf-8') if isinstance(v, six.string_types): v = v.encode('utf-8') hasher.update(k) hasher.update(v) return base64.b64encode(hasher.digest()).decode('utf-8') def get_auth_state(self): """Retrieve the current authentication state for the plugin. Retrieve any internal state that represents the authenticated plugin. This should not fetch any new data if it is not present. :returns: a string that can be stored or None if there is no auth state present in the plugin. This string can be reloaded with set_auth_state to set the same authentication. :rtype: str or None if no auth present. """ if self.auth_ref: data = {'auth_token': self.auth_ref.auth_token, 'body': self.auth_ref._data} return json.dumps(data) def set_auth_state(self, data): """Install existing authentication state for a plugin. Take the output of get_auth_state and install that authentication state into the current authentication plugin. """ if data: auth_data = json.loads(data) self.auth_ref = access.create(body=auth_data['body'], auth_token=auth_data['auth_token']) else: self.auth_ref = None keystoneauth-2.4.1/keystoneauth1/identity/generic/000077500000000000000000000000001271245473000223175ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/identity/generic/__init__.py000066400000000000000000000015121271245473000244270ustar00rootroot00000000000000# 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 keystoneauth1.identity.generic.base import BaseGenericPlugin # noqa from keystoneauth1.identity.generic.password import Password # noqa from keystoneauth1.identity.generic.token import Token # noqa __all__ = ('BaseGenericPlugin', 'Password', 'Token', ) keystoneauth-2.4.1/keystoneauth1/identity/generic/base.py000066400000000000000000000161361271245473000236120ustar00rootroot00000000000000# 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 abc import six import six.moves.urllib.parse as urlparse from keystoneauth1 import _utils as utils from keystoneauth1 import discover from keystoneauth1 import exceptions from keystoneauth1.identity import base LOG = utils.get_logger(__name__) @six.add_metaclass(abc.ABCMeta) class BaseGenericPlugin(base.BaseIdentityPlugin): """An identity plugin that is not version dependant. Internally we will construct a version dependant plugin with the resolved URL and then proxy all calls from the base plugin to the versioned one. """ def __init__(self, auth_url, tenant_id=None, tenant_name=None, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None, domain_id=None, domain_name=None, trust_id=None, default_domain_id=None, default_domain_name=None): super(BaseGenericPlugin, self).__init__(auth_url=auth_url) self._project_id = project_id or tenant_id self._project_name = project_name or tenant_name self._project_domain_id = project_domain_id self._project_domain_name = project_domain_name self._domain_id = domain_id self._domain_name = domain_name self._trust_id = trust_id self._default_domain_id = default_domain_id self._default_domain_name = default_domain_name self._plugin = None @abc.abstractmethod def create_plugin(self, session, version, url, raw_status=None): """Create a plugin from the given paramters. This function will be called multiple times with the version and url of a potential endpoint. If a plugin can be constructed that fits the params then it should return it. If not return None and then another call will be made with other available URLs. :param session: A session object. :type session: keystonauth.session.Session :param tuple version: A tuple of the API version at the URL. :param string url: The base URL for this version. :param string raw_status: The status that was in the discovery field. :returns: A plugin that can match the parameters or None if nothing. """ return None @property def _has_domain_scope(self): """Are there domain parameters. Domain parameters are v3 only so returns if any are set. :returns: True if a domain parameter is set, false otherwise. """ return any([self._domain_id, self._domain_name, self._project_domain_id, self._project_domain_name]) @property def _v2_params(self): """Parameters that are common to v2 plugins.""" return {'trust_id': self._trust_id, 'tenant_id': self._project_id, 'tenant_name': self._project_name} @property def _v3_params(self): """Parameters that are common to v3 plugins.""" pr_domain_id = self._project_domain_id or self._default_domain_id pr_domain_name = self._project_domain_name or self._default_domain_name return {'trust_id': self._trust_id, 'project_id': self._project_id, 'project_name': self._project_name, 'project_domain_id': pr_domain_id, 'project_domain_name': pr_domain_name, 'domain_id': self._domain_id, 'domain_name': self._domain_name} def _do_create_plugin(self, session): plugin = None try: disc = self.get_discovery(session, self.auth_url, authenticated=False) except (exceptions.DiscoveryFailure, exceptions.HttpError, exceptions.ConnectionError): LOG.warning('Discovering versions from the identity service ' 'failed when creating the password plugin. ' 'Attempting to determine version from URL.') url_parts = urlparse.urlparse(self.auth_url) path = url_parts.path.lower() if path.startswith('/v2.0'): if self._has_domain_scope: raise exceptions.DiscoveryFailure( 'Cannot use v2 authentication with domain scope') plugin = self.create_plugin(session, (2, 0), self.auth_url) elif path.startswith('/v3'): plugin = self.create_plugin(session, (3, 0), self.auth_url) else: # NOTE(jamielennox): version_data is always in oldest to newest # order. This is fine normally because we explicitly skip v2 below # if there is domain data present. With default_domain params # though we want a v3 plugin if available and fall back to v2 so we # have to process in reverse order. FIXME(jamielennox): if we ever # go for another version we should reverse this logic as we always # want to favour the newest available version. reverse = self._default_domain_id or self._default_domain_name disc_data = disc.version_data(reverse=bool(reverse)) v2_with_domain_scope = False for data in disc_data: version = data['version'] if (discover.version_match((2,), version) and self._has_domain_scope): # NOTE(jamielennox): if there are domain parameters there # is no point even trying against v2 APIs. v2_with_domain_scope = True continue plugin = self.create_plugin(session, version, data['url'], raw_status=data['raw_status']) if plugin: break if not plugin and v2_with_domain_scope: raise exceptions.DiscoveryFailure( 'Cannot use v2 authentication with domain scope') if plugin: return plugin # so there were no URLs that i could use for auth of any version. raise exceptions.DiscoveryFailure('Could not determine a suitable URL ' 'for the plugin') def get_auth_ref(self, session, **kwargs): if not self._plugin: self._plugin = self._do_create_plugin(session) return self._plugin.get_auth_ref(session, **kwargs) keystoneauth-2.4.1/keystoneauth1/identity/generic/password.py000066400000000000000000000051411271245473000245340ustar00rootroot00000000000000# 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 positional import positional from keystoneauth1 import discover from keystoneauth1.identity.generic import base from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 class Password(base.BaseGenericPlugin): """A common user/password authentication plugin. :param string username: Username for authentication. :param string user_id: User ID for authentication. :param string password: Password for authentication. :param string user_domain_id: User's domain ID for authentication. :param string user_domain_name: User's domain name for authentication. """ @positional() def __init__(self, auth_url, username=None, user_id=None, password=None, user_domain_id=None, user_domain_name=None, **kwargs): super(Password, self).__init__(auth_url=auth_url, **kwargs) self._username = username self._user_id = user_id self._password = password self._user_domain_id = user_domain_id self._user_domain_name = user_domain_name def create_plugin(self, session, version, url, raw_status=None): if discover.version_match((2,), version): if self._user_domain_id or self._user_domain_name: return None return v2.Password(auth_url=url, user_id=self._user_id, username=self._username, password=self._password, **self._v2_params) elif discover.version_match((3,), version): u_domain_id = self._user_domain_id or self._default_domain_id u_domain_name = self._user_domain_name or self._default_domain_name return v3.Password(auth_url=url, user_id=self._user_id, username=self._username, user_domain_id=u_domain_id, user_domain_name=u_domain_name, password=self._password, **self._v3_params) keystoneauth-2.4.1/keystoneauth1/identity/generic/token.py000066400000000000000000000025161271245473000240150ustar00rootroot00000000000000# 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 keystoneauth1 import _utils as utils from keystoneauth1 import discover from keystoneauth1.identity.generic import base from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 LOG = utils.get_logger(__name__) class Token(base.BaseGenericPlugin): """Generic token auth plugin. :param string token: Token for authentication. """ def __init__(self, auth_url, token=None, **kwargs): super(Token, self).__init__(auth_url, **kwargs) self._token = token def create_plugin(self, session, version, url, raw_status=None): if discover.version_match((2,), version): return v2.Token(url, self._token, **self._v2_params) elif discover.version_match((3,), version): return v3.Token(url, self._token, **self._v3_params) keystoneauth-2.4.1/keystoneauth1/identity/v2.py000066400000000000000000000143511271245473000216100ustar00rootroot00000000000000# 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 abc from positional import positional import six from keystoneauth1 import _utils as utils from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.identity import base _logger = utils.get_logger(__name__) @six.add_metaclass(abc.ABCMeta) class Auth(base.BaseIdentityPlugin): """Identity V2 Authentication Plugin. :param string auth_url: Identity service endpoint for authorization. :param string trust_id: Trust ID for trust scoping. :param string tenant_id: Tenant ID for project scoping. :param string tenant_name: Tenant name for project scoping. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True """ @positional() def __init__(self, auth_url, trust_id=None, tenant_id=None, tenant_name=None, reauthenticate=True): super(Auth, self).__init__(auth_url=auth_url, reauthenticate=reauthenticate) self.trust_id = trust_id self.tenant_id = tenant_id self.tenant_name = tenant_name def get_auth_ref(self, session, **kwargs): headers = {'Accept': 'application/json'} url = self.auth_url.rstrip('/') + '/tokens' params = {'auth': self.get_auth_data(headers)} if self.tenant_id: params['auth']['tenantId'] = self.tenant_id elif self.tenant_name: params['auth']['tenantName'] = self.tenant_name if self.trust_id: params['auth']['trust_id'] = self.trust_id _logger.debug('Making authentication request to %s', url) resp = session.post(url, json=params, headers=headers, authenticated=False, log=False) try: resp_data = resp.json() except ValueError: raise exceptions.InvalidResponse(response=resp) if 'access' not in resp_data: raise exceptions.InvalidResponse(response=resp) return access.AccessInfoV2(resp_data) @abc.abstractmethod def get_auth_data(self, headers=None): """Return the authentication section of an auth plugin. :param dict headers: The headers that will be sent with the auth request if a plugin needs to add to them. :return: A dict of authentication data for the auth type. :rtype: dict """ @property def has_scope_parameters(self): """Does the plugin have parameters that will create a scoped token""" return self.tenant_id or self.tenant_name or self.trust_id _NOT_PASSED = object() class Password(Auth): """A plugin for authenticating with a username and password. A username or user_id must be provided. :param string auth_url: Identity service endpoint for authorization. :param string username: Username for authentication. :param string password: Password for authentication. :param string user_id: User ID for authentication. :param string trust_id: Trust ID for trust scoping. :param string tenant_id: Tenant ID for tenant scoping. :param string tenant_name: Tenant name for tenant scoping. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True :raises TypeError: if a user_id or username is not provided. """ @positional(4) def __init__(self, auth_url, username=_NOT_PASSED, password=None, user_id=_NOT_PASSED, **kwargs): super(Password, self).__init__(auth_url, **kwargs) if username is _NOT_PASSED and user_id is _NOT_PASSED: msg = 'You need to specify either a username or user_id' raise TypeError(msg) if username is _NOT_PASSED: username = None if user_id is _NOT_PASSED: user_id = None self.user_id = user_id self.username = username self.password = password def get_auth_data(self, headers=None): auth = {'password': self.password} if self.username: auth['username'] = self.username elif self.user_id: auth['userId'] = self.user_id return {'passwordCredentials': auth} def get_cache_id_elements(self): return {'username': self.username, 'user_id': self.user_id, 'password': self.password, 'auth_url': self.auth_url, 'tenant_id': self.tenant_id, 'tenant_name': self.tenant_name, 'trust_id': self.trust_id} class Token(Auth): """A plugin for authenticating with an existing token. :param string auth_url: Identity service endpoint for authorization. :param string token: Existing token for authentication. :param string tenant_id: Tenant ID for tenant scoping. :param string tenant_name: Tenant name for tenant scoping. :param string trust_id: Trust ID for trust scoping. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True """ def __init__(self, auth_url, token, **kwargs): super(Token, self).__init__(auth_url, **kwargs) self.token = token def get_auth_data(self, headers=None): if headers is not None: headers['X-Auth-Token'] = self.token return {'token': {'id': self.token}} def get_cache_id_elements(self): return {'token': self.token, 'auth_url': self.auth_url, 'tenant_id': self.tenant_id, 'tenant_name': self.tenant_name, 'trust_id': self.trust_id} keystoneauth-2.4.1/keystoneauth1/identity/v3/000077500000000000000000000000001271245473000212335ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/identity/v3/__init__.py000066400000000000000000000022631271245473000233470ustar00rootroot00000000000000# 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 keystoneauth1.identity.v3.base import * # noqa from keystoneauth1.identity.v3.federation import * # noqa from keystoneauth1.identity.v3.k2k import * # noqa from keystoneauth1.identity.v3.password import * # noqa from keystoneauth1.identity.v3.token import * # noqa from keystoneauth1.identity.v3.oidc import * # noqa __all__ = ('Auth', 'AuthConstructor', 'AuthMethod', 'BaseAuth', 'FederationBaseAuth', 'Keystone2Keystone', 'Password', 'PasswordMethod', 'Token', 'TokenMethod' 'OidcAuthorizationCode', 'OidcPassword') keystoneauth-2.4.1/keystoneauth1/identity/v3/base.py000066400000000000000000000261611271245473000225250ustar00rootroot00000000000000# 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 abc from positional import positional import six from keystoneauth1 import _utils as utils from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.identity import base _logger = utils.get_logger(__name__) __all__ = ('Auth', 'AuthMethod', 'AuthConstructor', 'BaseAuth') @six.add_metaclass(abc.ABCMeta) class BaseAuth(base.BaseIdentityPlugin): """Identity V3 Authentication Plugin. :param string auth_url: Identity service endpoint for authentication. :param list auth_methods: A collection of methods to authenticate with. :param string trust_id: Trust ID for trust scoping. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True :param bool include_catalog: Include the service catalog in the returned token. (optional) default True. """ @positional() def __init__(self, auth_url, trust_id=None, domain_id=None, domain_name=None, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None, reauthenticate=True, include_catalog=True): super(BaseAuth, self).__init__(auth_url=auth_url, reauthenticate=reauthenticate) self.trust_id = trust_id self.domain_id = domain_id self.domain_name = domain_name self.project_id = project_id self.project_name = project_name self.project_domain_id = project_domain_id self.project_domain_name = project_domain_name self.include_catalog = include_catalog @property def token_url(self): """The full URL where we will send authentication data.""" return '%s/auth/tokens' % self.auth_url.rstrip('/') @abc.abstractmethod def get_auth_ref(self, session, **kwargs): return None @property def has_scope_parameters(self): """Does the plugin have parameters that will create a scoped token""" return (self.domain_id or self.domain_name or self.project_id or self.project_name or self.trust_id) class Auth(BaseAuth): """Identity V3 Authentication Plugin. :param string auth_url: Identity service endpoint for authentication. :param list auth_methods: A collection of methods to authenticate with. :param string trust_id: Trust ID for trust scoping. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True :param bool include_catalog: Include the service catalog in the returned token. (optional) default True. :param bool unscoped: Force the return of an unscoped token. This will make the keystone server return an unscoped token even if a default_project_id is set for this user. """ def __init__(self, auth_url, auth_methods, **kwargs): self.unscoped = kwargs.pop('unscoped', False) super(Auth, self).__init__(auth_url=auth_url, **kwargs) self.auth_methods = auth_methods def get_auth_ref(self, session, **kwargs): headers = {'Accept': 'application/json'} body = {'auth': {'identity': {}}} ident = body['auth']['identity'] rkwargs = {} for method in self.auth_methods: name, auth_data = method.get_auth_data(session, self, headers, request_kwargs=rkwargs) ident.setdefault('methods', []).append(name) ident[name] = auth_data if not ident: raise exceptions.AuthorizationFailure( 'Authentication method required (e.g. password)') mutual_exclusion = [bool(self.domain_id or self.domain_name), bool(self.project_id or self.project_name), bool(self.trust_id), bool(self.unscoped)] if sum(mutual_exclusion) > 1: raise exceptions.AuthorizationFailure( 'Authentication cannot be scoped to multiple targets. Pick ' 'one of: project, domain, trust or unscoped') if self.domain_id: body['auth']['scope'] = {'domain': {'id': self.domain_id}} elif self.domain_name: body['auth']['scope'] = {'domain': {'name': self.domain_name}} elif self.project_id: body['auth']['scope'] = {'project': {'id': self.project_id}} elif self.project_name: scope = body['auth']['scope'] = {'project': {}} scope['project']['name'] = self.project_name if self.project_domain_id: scope['project']['domain'] = {'id': self.project_domain_id} elif self.project_domain_name: scope['project']['domain'] = {'name': self.project_domain_name} elif self.trust_id: body['auth']['scope'] = {'OS-TRUST:trust': {'id': self.trust_id}} elif self.unscoped: body['auth']['scope'] = {'unscoped': {}} # NOTE(jamielennox): we add nocatalog here rather than in token_url # directly as some federation plugins require the base token_url token_url = self.token_url if not self.include_catalog: token_url += '?nocatalog' _logger.debug('Making authentication request to %s', token_url) resp = session.post(token_url, json=body, headers=headers, authenticated=False, log=False, **rkwargs) try: resp_data = resp.json() except ValueError: raise exceptions.InvalidResponse(response=resp) if 'token' not in resp_data: raise exceptions.InvalidResponse(response=resp) return access.AccessInfoV3(auth_token=resp.headers['X-Subject-Token'], body=resp_data) def get_cache_id_elements(self): if not self.auth_methods: return None params = {'auth_url': self.auth_url, 'domain_id': self.domain_id, 'domain_name': self.domain_name, 'project_id': self.project_id, 'project_name': self.project_name, 'project_domain_id': self.project_domain_id, 'project_domain_name': self.project_domain_name, 'trust_id': self.trust_id} for method in self.auth_methods: try: elements = method.get_cache_id_elements() except NotImplemented: return None params.update(elements) return params @six.add_metaclass(abc.ABCMeta) class AuthMethod(object): """One part of a V3 Authentication strategy. V3 Tokens allow multiple methods to be presented when authentication against the server. Each one of these methods is implemented by an AuthMethod. Note: When implementing an AuthMethod use the method_parameters and do not use positional arguments. Otherwise they can't be picked up by the factory method and don't work as well with AuthConstructors. """ _method_parameters = [] def __init__(self, **kwargs): for param in self._method_parameters: setattr(self, param, kwargs.pop(param, None)) if kwargs: msg = "Unexpected Attributes: %s" % ", ".join(kwargs.keys()) raise AttributeError(msg) @classmethod def _extract_kwargs(cls, kwargs): """Remove parameters related to this method from other kwargs.""" return dict([(p, kwargs.pop(p, None)) for p in cls._method_parameters]) @abc.abstractmethod def get_auth_data(self, session, auth, headers, **kwargs): """Return the authentication section of an auth plugin. :param session: The communication session. :type session: keystonauth.session.Session :param Auth auth: The auth plugin calling the method. :param dict headers: The headers that will be sent with the auth request if a plugin needs to add to them. :return: The identifier of this plugin and a dict of authentication data for the auth type. :rtype: tuple(string, dict) """ def get_cache_id_elements(self): """Get the elements for this auth method that make it unique. These elements will be used as part of the :py:meth:`keystoneauth1.plugin.BaseIdentityPlugin.get_cache_id` to allow caching of the auth plugin. Plugins should override this if they want to allow caching of their state. To avoid collision or overrides the keys of the returned dictionary should be prefixed with the plugin identifier. For example the password plugin returns its username value as 'password_username'. """ raise NotImplemented() @six.add_metaclass(abc.ABCMeta) class AuthConstructor(Auth): """Abstract base class for creating an Auth Plugin. The Auth Plugin created contains only one authentication method. This is generally the required usage. An AuthConstructor creates an AuthMethod based on the method's arguments and the auth_method_class defined by the plugin. It then creates the auth plugin with only that authentication method. """ _auth_method_class = None def __init__(self, auth_url, *args, **kwargs): method_kwargs = self._auth_method_class._extract_kwargs(kwargs) method = self._auth_method_class(*args, **method_kwargs) super(AuthConstructor, self).__init__(auth_url, [method], **kwargs) keystoneauth-2.4.1/keystoneauth1/identity/v3/federation.py000066400000000000000000000102121271245473000237210ustar00rootroot00000000000000# 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 abc import six from keystoneauth1.identity.v3 import base from keystoneauth1.identity.v3 import token __all__ = ('FederationBaseAuth',) @six.add_metaclass(abc.ABCMeta) class _Rescoped(base.BaseAuth): """A plugin that is always going to go through a rescope process. The original keystone plugins could simply pass a project or domain to along with the credentials and get a scoped token. For federation, K2K and newer mechanisms we always get an unscoped token first and then rescope. This is currently not public as it's generally an abstraction of a flow used by plugins within keystoneauth1. It also cannot go in base as it depends on token.Token for rescoping which would create a circular dependency. """ rescoping_plugin = token.Token def _get_scoping_data(self): return {'trust_id': self.trust_id, 'domain_id': self.domain_id, 'domain_name': self.domain_name, 'project_id': self.project_id, 'project_name': self.project_name, 'project_domain_id': self.project_domain_id, 'project_domain_name': self.project_domain_name} def get_auth_ref(self, session, **kwargs): """Authenticate retrieve token information. This is a multi-step process where a client does federated authn receives an unscoped token. If an unscoped token is successfully received and scoping information is present then the token is rescoped to that target. :param session: a session object to send out HTTP requests. :type session: keystonauth.session.Session :returns: a token data representation :rtype: :py:class:`keystonauth.access.AccessInfo` """ auth_ref = self.get_unscoped_auth_ref(session) scoping = self._get_scoping_data() if any(scoping.values()): token_plugin = self.rescoping_plugin(self.auth_url, token=auth_ref.auth_token, **scoping) auth_ref = token_plugin.get_auth_ref(session) return auth_ref @abc.abstractmethod def get_unscoped_auth_ref(self, session, **kwargs): """Fetch unscoped federated token.""" class FederationBaseAuth(_Rescoped): def __init__(self, auth_url, identity_provider, protocol, **kwargs): """Class constructor accepting following parameters: :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: name of the Identity Provider the client will authenticate against. This parameter will be used to build a dynamic URL used to obtain unscoped OpenStack token. :type identity_provider: string :param protocol: name of the protocol the client will authenticate against. :type protocol: string """ super(FederationBaseAuth, self).__init__(auth_url=auth_url, **kwargs) self.identity_provider = identity_provider self.protocol = protocol @property def federated_token_url(self): """Full URL where authorization data is sent.""" values = { 'host': self.auth_url.rstrip('/'), 'identity_provider': self.identity_provider, 'protocol': self.protocol } url = ("%(host)s/OS-FEDERATION/identity_providers/" "%(identity_provider)s/protocols/%(protocol)s/auth") url = url % values return url keystoneauth-2.4.1/keystoneauth1/identity/v3/k2k.py000066400000000000000000000151261271245473000223010ustar00rootroot00000000000000# 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 requests import six from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.identity.v3 import federation from keystoneauth1 import plugin __all__ = ('Keystone2Keystone',) class Keystone2Keystone(federation._Rescoped): """Plugin to execute the Keystone to Keyestone authentication flow. In this plugin, an ECP wrapped SAML assertion provided by a keystone Identity Provider (IdP) is used to request an OpenStack unscoped token from a keystone Service Provider (SP). :param base_plugin: Auth plugin already authenticated against the keystone IdP. :type base_plugin: keystoneauth1.identity.v3.base.BaseAuth :param service_provider: The Service Provider ID as returned by ServiceProviderManager.list() :type service_provider: str """ REQUEST_ECP_URL = '/auth/OS-FEDERATION/saml2/ecp' """Path where the ECP wrapped SAML assertion should be presented to the Keystone Service Provider.""" def __init__(self, base_plugin, service_provider, **kwargs): super(Keystone2Keystone, self).__init__(auth_url=None, **kwargs) self._local_cloud_plugin = base_plugin self._sp_id = service_provider @classmethod def _remote_auth_url(cls, auth_url): """Return auth_url of the remote Keystone Service Provider Remote cloud's auth_url is an endpoint for getting federated unscoped token, typically that would be ``https://remote.example.com:5000/v3/OS-FEDERATION/identity_providers/ /protocols//auth``. However we need to generate a real auth_url, used for token scoping. This function assumes there are static values today in the remote auth_url stored in the Service Provider attribute and those can be used as a delimiter. If the sp_auth_url doesn't comply with standard federation auth url the function will simply return whole string. :param auth_url: auth_url of the remote cloud :type auth_url: str :returns: auth_url of remote cloud where a token can be validated or scoped. :rtype: str """ PATTERN = '/OS-FEDERATION/' idx = auth_url.index(PATTERN) if PATTERN in auth_url else len(auth_url) return auth_url[:idx] def _get_ecp_assertion(self, session): body = { 'auth': { 'identity': { 'methods': ['token'], 'token': { 'id': self._local_cloud_plugin.get_token(session) } }, 'scope': { 'service_provider': { 'id': self._sp_id } } } } endpoint_filter = {'version': (3, 0), 'interface': plugin.AUTH_INTERFACE} headers = {'Accept': 'application/json'} resp = session.post(self.REQUEST_ECP_URL, json=body, auth=self._local_cloud_plugin, endpoint_filter=endpoint_filter, headers=headers, authenticated=False, raise_exc=False) # NOTE(marek-denis): I am not sure whether disabling exceptions in the # Session object and testing if resp.ok is sufficient. An alternative # would be catching locally all exceptions and reraising with custom # warning. if not resp.ok: msg = ("Error while requesting ECP wrapped assertion: response " "exit code: %(status_code)d, reason: %(err)s") msg = msg % {'status_code': resp.status_code, 'err': resp.reason} raise exceptions.AuthorizationFailure(msg) if not resp.text: raise exceptions.InvalidResponse(resp) return six.text_type(resp.text) def _send_service_provider_ecp_authn_response(self, session, sp_url, sp_auth_url): """Present ECP wrapped SAML assertion to the keystone SP. The assertion is issued by the keystone IdP and it is targeted to the keystone that will serve as Service Provider. :param session: a session object to send out HTTP requests. :param sp_url: URL where the ECP wrapped SAML assertion will be presented to the keystone SP. Usually, something like: https://sp.com/Shibboleth.sso/SAML2/ECP :type sp_url: str :param sp_auth_url: Federated authentication URL of the keystone SP. It is specified by IdP, for example: https://sp.com/v3/OS-FEDERATION/identity_providers/ idp_id/protocols/protocol_id/auth :type sp_auth_url: str """ response = session.post( sp_url, headers={'Content-Type': 'application/vnd.paos+xml'}, data=self._get_ecp_assertion(session), authenticated=False, redirect=False) # Don't follow HTTP specs - after the HTTP 302 response don't repeat # the call directed to the Location URL. In this case, this is an # indication that SAML2 session is now active and protected resource # can be accessed. if response.status_code == requests.codes['found']: response = session.get( sp_auth_url, headers={'Content-Type': 'application/vnd.paos+xml'}, authenticated=False) return response def get_unscoped_auth_ref(self, session, **kwargs): sp_auth_url = self._local_cloud_plugin.get_sp_auth_url( session, self._sp_id) sp_url = self._local_cloud_plugin.get_sp_url(session, self._sp_id) self.auth_url = self._remote_auth_url(sp_auth_url) response = self._send_service_provider_ecp_authn_response( session, sp_url, sp_auth_url) return access.create(resp=response) keystoneauth-2.4.1/keystoneauth1/identity/v3/oidc.py000066400000000000000000000263661271245473000225400ustar00rootroot00000000000000# 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 positional import positional from keystoneauth1 import access from keystoneauth1.identity.v3 import federation __all__ = ('OidcAuthorizationCode', 'OidcPassword') class _OidcBase(federation.FederationBaseAuth): """Base class for different OpenID Connect based flows The OpenID Connect specification can be found at:: ``http://openid.net/specs/openid-connect-core-1_0.html`` """ def __init__(self, auth_url, identity_provider, protocol, client_id, client_secret, access_token_endpoint, grant_type, access_token_type, **kwargs): """The OpenID Connect plugin expects the following: :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: Name of the Identity Provider the client will authenticate against :type identity_provider: string :param protocol: Protocol name as configured in keystone :type protocol: string :param client_id: OAuth 2.0 Client ID :type client_id: string :param client_secret: OAuth 2.0 Client Secret :type client_secret: string :param access_token_endpoint: OpenID Connect Provider Token Endpoint, for example: https://localhost:8020/oidc/OP/token :type access_token_endpoint: string :param grant_type: OpenID Connect grant type, it represents the flow that is used to talk to the OP. Valid values are: "authorization_code", "refresh_token", or "password". :type grant_type: string :param access_token_type: OAuth 2.0 Authorization Server Introspection token type, it is used to decide which type of token will be used when processing token introspection. Valid values are: "access_token" or "id_token" :type access_token_type: string """ super(_OidcBase, self).__init__(auth_url, identity_provider, protocol, **kwargs) self.client_id = client_id self.client_secret = client_secret self.access_token_endpoint = access_token_endpoint self.grant_type = grant_type self.access_token_type = access_token_type def _get_access_token(self, session, client_auth, payload, access_token_endpoint): """Exchange a variety of user supplied values for an access token. :param session: a session object to send out HTTP requests. :type session: keystoneauth.session.Session :param client_auth: a tuple representing client id and secret :type client_auth: tuple :param payload: a dict containing various OpenID Connect values, for example:: {'grant_type': 'password', 'username': self.username, 'password': self.password, 'scope': self.scope} :type payload: dict :param access_token_endpoint: URL to use to get an access token, for example: https://localhost/oidc/token :type access_token_endpoint: string """ op_response = session.post(self.access_token_endpoint, requests_auth=client_auth, data=payload, authenticated=False) return op_response def _get_keystone_token(self, session, headers, federated_token_url): """Exchange an acess token for a keystone token. By Sending the access token in an `Authorization: Bearer` header, to an OpenID Connect protected endpoint (Federated Token URL). The OpenID Connect server will use the access token to look up information about the authenticated user (this technique is called instrospection). The output of the instrospection will be an OpenID Connect Claim, that will be used against the mapping engine. Should the mapping engine succeed, a Keystone token will be presented to the user. :param session: a session object to send out HTTP requests. :type session: keystoneauth.session.Session :param headers: an Authorization header containing the access token. :type headers_: dict :param federated_auth_url: Protected URL for federated authentication, for example: https://localhost:5000/v3/\ OS-FEDERATION/identity_providers/bluepages/\ protocols/oidc/auth :type federated_auth_url: string """ auth_response = session.post(self.federated_token_url, headers=headers, authenticated=False) return auth_response class OidcPassword(_OidcBase): """Implementation for OpenID Connect Resource Owner Password Credential""" @positional(4) def __init__(self, auth_url, identity_provider, protocol, client_id, client_secret, access_token_endpoint, grant_type='password', access_token_type='access_token', username=None, password=None, scope='profile'): """The OpenID Password plugin expects the following: :param username: Username used to authenticate :type username: string :param password: Password used to authenticate :type password: string :param scope: OpenID Connect scope that is requested from OP, defaults to "profile", for example: "profile email" :type scope: string """ super(OidcPassword, self).__init__( auth_url=auth_url, identity_provider=identity_provider, protocol=protocol, client_id=client_id, client_secret=client_secret, access_token_endpoint=access_token_endpoint, grant_type=grant_type, access_token_type=access_token_type) self.username = username self.password = password self.scope = scope def get_unscoped_auth_ref(self, session): """Authenticate with OpenID Connect and get back claims. This is a multi-step process. First an access token must be retrieved, to do this, the username and password, the OpenID Connect client ID and secret, and the access token endpoint must be known. Secondly, we then exchange the access token upon accessing the protected Keystone endpoint (federated auth URL). This will trigger the OpenID Connect Provider to perform a user introspection and retrieve information (specified in the scope) about the user in the form of an OpenID Connect Claim. These claims will be sent to Keystone in the form of environment variables. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: a token data representation :rtype: :py:class:`keystoneauth1.access.AccessInfoV3` """ # get an access token client_auth = (self.client_id, self.client_secret) payload = {'grant_type': self.grant_type, 'username': self.username, 'password': self.password, 'scope': self.scope} response = self._get_access_token(session, client_auth, payload, self.access_token_endpoint) access_token = response.json()[self.access_token_type] # use access token against protected URL headers = {'Authorization': 'Bearer ' + access_token} response = self._get_keystone_token(session, headers, self.federated_token_url) # grab the unscoped token return access.create(resp=response) class OidcAuthorizationCode(_OidcBase): """Implementation for OpenID Connect Authorization Code""" @positional(4) def __init__(self, auth_url, identity_provider, protocol, client_id, client_secret, access_token_endpoint, grant_type='authorization_code', access_token_type='access_token', redirect_uri=None, code=None): """The OpenID Authorization Code plugin expects the following: :param redirect_uri: OpenID Connect Client Redirect URL :type redirect_uri: string :param code: OAuth 2.0 Authorization Code :type code: string """ super(OidcAuthorizationCode, self).__init__( auth_url=auth_url, identity_provider=identity_provider, protocol=protocol, client_id=client_id, client_secret=client_secret, access_token_endpoint=access_token_endpoint, grant_type=grant_type, access_token_type=access_token_type) self.redirect_uri = redirect_uri self.code = code def get_unscoped_auth_ref(self, session): """Authenticate with OpenID Connect and get back claims. This is a multi-step process. First an access token must be retrieved, to do this, an authorization code and redirect URL must be given. Secondly, we then exchange the access token upon accessing the protected Keystone endpoint (federated auth URL). This will trigger the OpenID Connect Provider to perform a user introspection and retrieve information (specified in the scope) about the user in the form of an OpenID Connect Claim. These claims will be sent to Keystone in the form of environment variables. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: a token data representation :rtype: :py:class:`keystoneauth1.access.AccessInfoV3` """ # get an access token client_auth = (self.client_id, self.client_secret) payload = {'grant_type': self.grant_type, 'redirect_uri': self.redirect_uri, 'code': self.code} response = self._get_access_token(session, client_auth, payload, self.access_token_endpoint) access_token = response.json()[self.access_token_type] # use access token against protected URL headers = {'Authorization': 'Bearer ' + access_token} response = self._get_keystone_token(session, headers, self.federated_token_url) # grab the unscoped token return access.create(resp=response) keystoneauth-2.4.1/keystoneauth1/identity/v3/password.py000066400000000000000000000057471271245473000234640ustar00rootroot00000000000000# 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 keystoneauth1.identity.v3 import base __all__ = ('PasswordMethod', 'Password') class PasswordMethod(base.AuthMethod): """Construct a User/Password based authentication method. :param string password: Password for authentication. :param string username: Username for authentication. :param string user_id: User ID for authentication. :param string user_domain_id: User's domain ID for authentication. :param string user_domain_name: User's domain name for authentication. """ _method_parameters = ['user_id', 'username', 'user_domain_id', 'user_domain_name', 'password'] def get_auth_data(self, session, auth, headers, **kwargs): user = {'password': self.password} if self.user_id: user['id'] = self.user_id elif self.username: user['name'] = self.username if self.user_domain_id: user['domain'] = {'id': self.user_domain_id} elif self.user_domain_name: user['domain'] = {'name': self.user_domain_name} return 'password', {'user': user} def get_cache_id_elements(self): return dict(('password_%s' % p, getattr(self, p)) for p in self._method_parameters) class Password(base.AuthConstructor): """A plugin for authenticating with a username and password. :param string auth_url: Identity service endpoint for authentication. :param string password: Password for authentication. :param string username: Username for authentication. :param string user_id: User ID for authentication. :param string user_domain_id: User's domain ID for authentication. :param string user_domain_name: User's domain name for authentication. :param string trust_id: Trust ID for trust scoping. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True """ _auth_method_class = PasswordMethod keystoneauth-2.4.1/keystoneauth1/identity/v3/token.py000066400000000000000000000040011271245473000227200ustar00rootroot00000000000000# 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 keystoneauth1.identity.v3 import base __all__ = ('TokenMethod', 'Token') class TokenMethod(base.AuthMethod): """Construct an Auth plugin to fetch a token from a token. :param string token: Token for authentication. """ _method_parameters = ['token'] def get_auth_data(self, session, auth, headers, **kwargs): headers['X-Auth-Token'] = self.token return 'token', {'id': self.token} def get_cache_id_elements(self): return {'token_token': self.token} class Token(base.AuthConstructor): """A plugin for authenticating with an existing Token. :param string auth_url: Identity service endpoint for authentication. :param string token: Token for authentication. :param string trust_id: Trust ID for trust scoping. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True """ _auth_method_class = TokenMethod def __init__(self, auth_url, token, **kwargs): super(Token, self).__init__(auth_url, token=token, **kwargs) keystoneauth-2.4.1/keystoneauth1/loading/000077500000000000000000000000001271245473000204675ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/loading/__init__.py000066400000000000000000000044301271245473000226010ustar00rootroot00000000000000# 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 keystoneauth1.loading.base import * # noqa from keystoneauth1.loading import cli from keystoneauth1.loading import conf from keystoneauth1.loading.identity import * # noqa from keystoneauth1.loading.opts import * # noqa from keystoneauth1.loading import session register_auth_argparse_arguments = cli.register_argparse_arguments load_auth_from_argparse_arguments = cli.load_from_argparse_arguments get_auth_common_conf_options = conf.get_common_conf_options get_auth_plugin_conf_options = conf.get_plugin_conf_options register_auth_conf_options = conf.register_conf_options load_auth_from_conf_options = conf.load_from_conf_options register_session_argparse_arguments = session.register_argparse_arguments load_session_from_argparse_arguments = session.load_from_argparse_arguments register_session_conf_options = session.register_conf_options load_session_from_conf_options = session.load_from_conf_options get_session_conf_options = session.get_conf_options __all__ = ( # loading.base 'BaseLoader', 'get_available_plugin_names', 'get_available_plugin_loaders', 'get_plugin_loader', 'PLUGIN_NAMESPACE', # loading.identity 'BaseIdentityLoader', 'BaseV2Loader', 'BaseV3Loader', 'BaseFederationLoader', 'BaseGenericLoader', # auth cli 'register_auth_argparse_arguments', 'load_auth_from_argparse_arguments', # auth conf 'get_auth_common_conf_options', 'get_auth_plugin_conf_options', 'register_auth_conf_options', 'load_auth_from_conf_options', # session 'register_session_argparse_arguments', 'load_session_from_argparse_arguments', 'register_session_conf_options', 'load_session_from_conf_options', 'get_session_conf_options', # loading.opts 'Opt', ) keystoneauth-2.4.1/keystoneauth1/loading/_plugins/000077500000000000000000000000001271245473000223075ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/loading/_plugins/__init__.py000066400000000000000000000000001271245473000244060ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/loading/_plugins/admin_token.py000066400000000000000000000022431271245473000251520ustar00rootroot00000000000000# 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 keystoneauth1 import loading from keystoneauth1 import token_endpoint class AdminToken(loading.BaseLoader): @property def plugin_class(self): return token_endpoint.Token def get_options(self): options = super(AdminToken, self).get_options() options.extend([ loading.Opt('endpoint', deprecated=[loading.Opt('url')], help='The endpoint that will always be used'), loading.Opt('token', secret=True, help='The token that will always be used'), ]) return options keystoneauth-2.4.1/keystoneauth1/loading/_plugins/identity/000077500000000000000000000000001271245473000241405ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/loading/_plugins/identity/__init__.py000066400000000000000000000000001271245473000262370ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/loading/_plugins/identity/generic.py000066400000000000000000000031531271245473000261300ustar00rootroot00000000000000# 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 keystoneauth1 import identity from keystoneauth1 import loading class Token(loading.BaseGenericLoader): @property def plugin_class(self): return identity.Token def get_options(self): options = super(Token, self).get_options() options.extend([ loading.Opt('token', secret=True, help='Token to authenticate with'), ]) return options class Password(loading.BaseGenericLoader): @property def plugin_class(self): return identity.Password def get_options(cls): options = super(Password, cls).get_options() options.extend([ loading.Opt('user-id', help='User id'), loading.Opt('username', help='Username', deprecated=[loading.Opt('user-name')]), loading.Opt('user-domain-id', help="User's domain id"), loading.Opt('user-domain-name', help="User's domain name"), loading.Opt('password', secret=True, help="User's password"), ]) return options keystoneauth-2.4.1/keystoneauth1/loading/_plugins/identity/v2.py000066400000000000000000000027121271245473000250430ustar00rootroot00000000000000# 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 keystoneauth1 import identity from keystoneauth1 import loading class Token(loading.BaseV2Loader): @property def plugin_class(self): return identity.V2Token def get_options(self): options = super(Token, self).get_options() options.extend([ loading.Opt('token', secret=True, help='Token'), ]) return options class Password(loading.BaseV2Loader): @property def plugin_class(self): return identity.V2Password def get_options(self): options = super(Password, self).get_options() options.extend([ loading.Opt('username', deprecated=[loading.Opt('user-name')], help='Username to login with'), loading.Opt('user-id', help='User ID to login with'), loading.Opt('password', secret=True, help='Password to use'), ]) return options keystoneauth-2.4.1/keystoneauth1/loading/_plugins/identity/v3.py000066400000000000000000000101501271245473000250370ustar00rootroot00000000000000# 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 keystoneauth1 import exceptions from keystoneauth1 import identity from keystoneauth1 import loading class Password(loading.BaseV3Loader): @property def plugin_class(self): return identity.V3Password def get_options(self): options = super(Password, self).get_options() options.extend([ loading.Opt('user-id', help='User ID'), loading.Opt('username', help='Username', deprecated=[loading.Opt('user-name')]), loading.Opt('user-domain-id', help="User's domain id"), loading.Opt('user-domain-name', help="User's domain name"), loading.Opt('password', secret=True, help="User's password"), ]) return options def load_from_options(self, **kwargs): if (kwargs.get('username') and not (kwargs.get('user_domain_name') or kwargs.get('user_domain_id'))): m = "You have provided a username. In the V3 identity API a " \ "username is only unique within a domain so you must " \ "also provide either a user_domain_id or user_domain_name." raise exceptions.OptionError(m) return super(Password, self).load_from_options(**kwargs) class Token(loading.BaseV3Loader): @property def plugin_class(self): return identity.V3Token def get_options(self): options = super(Token, self).get_options() options.extend([ loading.Opt('token', secret=True, help='Token to authenticate with'), ]) return options class _OpenIDConnectBase(loading.BaseFederationLoader): def get_options(self): options = super(_OpenIDConnectBase, self).get_options() options.extend([ loading.Opt('client-id', help='OAuth 2.0 Client ID'), loading.Opt('client-secret', secret=True, help='OAuth 2.0 Client Secret'), loading.Opt('access-token-endpoint', help='OpenID Connect Provider Token Endpoint'), loading.Opt('access-token-type', help='OAuth 2.0 Authorization Server Introspection ' 'token type, it is used to decide which type ' 'of token will be used when processing token ' 'introspection. Valid values are: ' '"access_token" or "id_token"'), ]) return options class OpenIDConnectPassword(_OpenIDConnectBase): @property def plugin_class(self): return identity.V3OidcPassword def get_options(self): options = super(OpenIDConnectPassword, self).get_options() options.extend([ loading.Opt('username', help='Username'), loading.Opt('password', secret=True, help='Password'), loading.Opt('openid-scope', default="profile", help='OpenID Connect scope that is requested from OP') ]) return options class OpenIDConnectAuthorizationCode(_OpenIDConnectBase): @property def plugin_class(self): return identity.V3OidcAuthorizationCode def get_options(self): options = super(OpenIDConnectAuthorizationCode, self).get_options() options.extend([ loading.Opt('redirect-uri', help='OpenID Connect Redirect URL'), loading.Opt('authorization-code', secret=True, help='OAuth 2.0 Authorization Code'), ]) return options keystoneauth-2.4.1/keystoneauth1/loading/base.py000066400000000000000000000114111271245473000217510ustar00rootroot00000000000000# 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 abc import six import stevedore from keystoneauth1 import exceptions PLUGIN_NAMESPACE = 'keystoneauth1.plugin' __all__ = ('get_available_plugin_names', 'get_available_plugin_loaders', 'get_plugin_loader', 'get_plugin_options', 'BaseLoader', 'PLUGIN_NAMESPACE') def get_available_plugin_names(): """Get the names of all the plugins that are available on the system. This is particularly useful for help and error text to prompt a user for example what plugins they may specify. :returns: A list of names. :rtype: frozenset """ mgr = stevedore.ExtensionManager(namespace=PLUGIN_NAMESPACE) return frozenset(mgr.names()) def get_available_plugin_loaders(): """Retrieve all the plugin classes available on the system. :returns: A dict with plugin entrypoint name as the key and the plugin loader as the value. :rtype: dict """ mgr = stevedore.ExtensionManager(namespace=PLUGIN_NAMESPACE, invoke_on_load=True, propagate_map_exceptions=True) return dict(mgr.map(lambda ext: (ext.entry_point.name, ext.obj))) def get_plugin_loader(name): """Retrieve a plugin class by its entrypoint name. :param str name: The name of the object to get. :returns: An auth plugin class. :rtype: :py:class:`keystoneauth1.loading.BaseLoader` :raises keystonauth.exceptions.NoMatchingPlugin: if a plugin cannot be created. """ try: mgr = stevedore.DriverManager(namespace=PLUGIN_NAMESPACE, invoke_on_load=True, name=name) except RuntimeError: raise exceptions.NoMatchingPlugin(name) return mgr.driver def get_plugin_options(name): """Get the options for a specific plugin. This will be the list of options that is registered and loaded by the specified plugin. :returns: A list of :py:class:`keystoneauth1.loading.Opt` options. :raises keystonauth.exceptions.NoMatchingPlugin: if a plugin cannot be created. """ return get_plugin_loader(name).get_options() @six.add_metaclass(abc.ABCMeta) class BaseLoader(object): @abc.abstractproperty def plugin_class(self): raise NotImplemented() @abc.abstractmethod def get_options(self): """Return the list of parameters associated with the auth plugin. This list may be used to generate CLI or config arguments. :returns: A list of Param objects describing available plugin parameters. :rtype: list """ return [] def load_from_options(self, **kwargs): """Create a plugin from the arguments retrieved from get_options. A client can override this function to do argument validation or to handle differences between the registered options and what is required to create the plugin. """ missing_required = [o for o in self.get_options() if o.required and kwargs.get(o.dest) is None] if missing_required: raise exceptions.MissingRequiredOptions(missing_required) return self.plugin_class(**kwargs) def load_from_options_getter(self, getter, **kwargs): """Load a plugin from a getter function that returns appropriate values To handle cases other than the provided CONF and CLI loading you can specify a custom loader function that will be queried for the option value. The getter is a function that takes a :py:class:`keystoneauth1.loading.Opt` and returns a value to load with. :param getter: A function that returns a value for the given opt. :type getter: callable :returns: An authentication Plugin. :rtype: :py:class:`keystoneauth1.plugin.BaseAuthPlugin` """ for opt in (o for o in self.get_options() if o.dest not in kwargs): val = getter(opt) if val is not None: val = opt.type(val) kwargs[opt.dest] = val return self.load_from_options(**kwargs) keystoneauth-2.4.1/keystoneauth1/loading/cli.py000066400000000000000000000073751271245473000216240ustar00rootroot00000000000000# 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 argparse import os from positional import positional from keystoneauth1.loading import base __all__ = ('register_argparse_arguments', 'load_from_argparse_arguments') def _register_plugin_argparse_arguments(parser, plugin): for opt in plugin.get_options(): parser.add_argument(*opt.argparse_args, default=opt.argparse_default, metavar=opt.metavar, help=opt.help, dest='os_%s' % opt.dest) @positional() def register_argparse_arguments(parser, argv, default=None): """Register CLI options needed to create a plugin. The function inspects the provided arguments so that it can also register the options required for that specific plugin if available. :param argparse.ArgumentParser: the parser to attach argparse options to. :param list argv: the arguments provided to the appliation. :param str/class default: a default plugin name or a plugin object to use if one isn't specified by the CLI. default: None. :returns: The plugin class that will be loaded or None if not provided. :rtype: :py:class:`keystonauth.auth.BaseAuthPlugin` :raises keystonauth.exceptions.NoMatchingPlugin: if a plugin cannot be created. """ in_parser = argparse.ArgumentParser(add_help=False) env_plugin = os.environ.get('OS_AUTH_TYPE', os.environ.get('OS_AUTH_PLUGIN', default)) for p in (in_parser, parser): p.add_argument('--os-auth-type', '--os-auth-plugin', metavar='', default=env_plugin, help='Authentication type to use') options, _args = in_parser.parse_known_args(argv) if not options.os_auth_type: return None if isinstance(options.os_auth_type, base.BaseLoader): msg = 'Default Authentication options' plugin = options.os_auth_type else: msg = 'Options specific to the %s plugin.' % options.os_auth_type plugin = base.get_plugin_loader(options.os_auth_type) group = parser.add_argument_group('Authentication Options', msg) _register_plugin_argparse_arguments(group, plugin) return plugin def load_from_argparse_arguments(namespace, **kwargs): """Retrieve the created plugin from the completed argparse results. Loads and creates the auth plugin from the information parsed from the command line by argparse. :param Namespace namespace: The result from CLI parsing. :returns: An auth plugin, or None if a name is not provided. :rtype: :py:class:`keystonauth.auth.BaseAuthPlugin` :raises keystonauth.exceptions.NoMatchingPlugin: if a plugin cannot be created. """ if not namespace.os_auth_type: return None if isinstance(namespace.os_auth_type, type): plugin = namespace.os_auth_type else: plugin = base.get_plugin_loader(namespace.os_auth_type) def _getter(opt): return getattr(namespace, 'os_%s' % opt.dest) return plugin.load_from_options_getter(_getter) keystoneauth-2.4.1/keystoneauth1/loading/conf.py000066400000000000000000000115311271245473000217670ustar00rootroot00000000000000# 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 keystoneauth1.loading import base from keystoneauth1.loading import opts _AUTH_TYPE_OPT = opts.Opt('auth_type', deprecated=[opts.Opt('auth_plugin')], help='Authentication type to load') _section_help = 'Config Section from which to load plugin specific options' _AUTH_SECTION_OPT = opts.Opt('auth_section', help=_section_help) __all__ = ('get_common_conf_options', 'get_plugin_conf_options', 'register_conf_options', 'load_from_conf_options') def get_common_conf_options(): """Get the oslo_config options common for all auth plugins. These may be useful without being registered for config file generation or to manipulate the options before registering them yourself. The options that are set are: :auth_type: The name of the pluign to load. :auth_section: The config file section to load options from. :returns: A list of oslo_config options. """ return [_AUTH_TYPE_OPT._to_oslo_opt(), _AUTH_SECTION_OPT._to_oslo_opt()] def get_plugin_conf_options(plugin): """Get the oslo_config options for a specific plugin. This will be the list of config options that is registered and loaded by the specified plugin. :param plugin: The name of the plugin loader or a plugin loader object :type plugin: str or keystoneauth1._loading.BaseLoader :returns: A list of oslo_config options. """ try: getter = plugin.get_options except AttributeError: opts = base.get_plugin_options(plugin) else: opts = getter() return [o._to_oslo_opt() for o in opts] def register_conf_options(conf, group): """Register the oslo_config options that are needed for a plugin. This only registers the basic options shared by all plugins. Options that are specific to a plugin are loaded just before they are read. The defined options are: - auth_type: the name of the auth plugin that will be used for authentication. - auth_section: the group from which further auth plugin options should be taken. If section is not provided then the auth plugin options will be taken from the same group as provided in the parameters. :param conf: config object to register with. :type conf: oslo_config.cfg.ConfigOpts :param string group: The ini group to register options in. """ conf.register_opt(_AUTH_SECTION_OPT._to_oslo_opt(), group=group) # NOTE(jamielennox): plugins are allowed to specify a 'section' which is # the group that auth options should be taken from. If not present they # come from the same as the base options were registered in. If present # then the auth_plugin option may be read from that section so add that # option. if conf[group].auth_section: group = conf[group].auth_section conf.register_opt(_AUTH_TYPE_OPT._to_oslo_opt(), group=group) def load_from_conf_options(conf, group, **kwargs): """Load a plugin from an oslo_config CONF object. Each plugin will register their own required options and so there is no standard list and the plugin should be consulted. The base options should have been registered with register_conf_options before this function is called. :param conf: A conf object. :type conf: oslo_config.cfg.ConfigOpts :param string group: The group name that options should be read from. :returns: An authentication Plugin or None if a name is not provided :rtype: :py:class:`keystonauth.auth.BaseAuthPlugin` :raises keystonauth.exceptions.NoMatchingPlugin: if a plugin cannot be created. """ # NOTE(jamielennox): plugins are allowed to specify a 'section' which is # the group that auth options should be taken from. If not present they # come from the same as the base options were registered in. if conf[group].auth_section: group = conf[group].auth_section name = conf[group].auth_type if not name: return None plugin = base.get_plugin_loader(name) plugin_opts = plugin.get_options() oslo_opts = [o._to_oslo_opt() for o in plugin_opts] conf.register_opts(oslo_opts, group=group) def _getter(opt): return conf[group][opt.dest] return plugin.load_from_options_getter(_getter, **kwargs) keystoneauth-2.4.1/keystoneauth1/loading/identity.py000066400000000000000000000140041271245473000226710ustar00rootroot00000000000000# 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 keystoneauth1 import exceptions from keystoneauth1.loading import base from keystoneauth1.loading import opts __all__ = ('BaseIdentityLoader', 'BaseV2Loader', 'BaseV3Loader', 'BaseFederationLoader', 'BaseGenericLoader') class BaseIdentityLoader(base.BaseLoader): """Base Option handling for identity plugins. This class defines options and handling that should be common across all plugins that are developed against the OpenStack identity service. It provides the options expected by the :py:class:`keystoneauth1.identity.BaseIdentityPlugin` class. """ def get_options(self): options = super(BaseIdentityLoader, self).get_options() options.extend([ opts.Opt('auth-url', required=True, help='Authentication URL'), ]) return options class BaseV2Loader(BaseIdentityLoader): """Base Option handling for identity plugins. This class defines options and handling that should be common to the V2 identity API. It provides the options expected by the :py:class:`keystoneauth1.identity.v2.Auth` class. """ def get_options(self): options = super(BaseV2Loader, self).get_options() options.extend([ opts.Opt('tenant-id', help='Tenant ID'), opts.Opt('tenant-name', help='Tenant Name'), opts.Opt('trust-id', help='Trust ID'), ]) return options class BaseV3Loader(BaseIdentityLoader): """Base Option handling for identity plugins. This class defines options and handling that should be common to the V3 identity API. It provides the options expected by the :py:class:`keystoneauth1.identity.v3.Auth` class. """ def get_options(self): options = super(BaseV3Loader, self).get_options() options.extend([ opts.Opt('domain-id', help='Domain ID to scope to'), opts.Opt('domain-name', help='Domain name to scope to'), opts.Opt('project-id', help='Project ID to scope to'), opts.Opt('project-name', help='Project name to scope to'), opts.Opt('project-domain-id', help='Domain ID containing project'), opts.Opt('project-domain-name', help='Domain name containing project'), opts.Opt('trust-id', help='Trust ID'), ]) return options def load_from_options(self, **kwargs): if (kwargs.get('project_name') and not (kwargs.get('project_domain_name') or kwargs.get('project_domain_id'))): m = "You have provided a project_name. In the V3 identity API a " \ "project_name is only unique within a domain so you must " \ "also provide either a project_domain_id or " \ "project_domain_name." raise exceptions.OptionError(m) return super(BaseV3Loader, self).load_from_options(**kwargs) class BaseFederationLoader(BaseV3Loader): """Base Option handling for federation plugins. This class defines options and handling that should be common to the V3 identity federation API. It provides the options expected by the :py:class:`keystoneauth1.identity.v3.FederationBaseAuth` class. """ def get_options(self): options = super(BaseFederationLoader, self).get_options() options.extend([ opts.Opt('identity-provider', help="Identity Provider's name", required=True), opts.Opt('protocol', help='Protocol for federated plugin', required=True), ]) return options class BaseGenericLoader(BaseIdentityLoader): """Base Option handling for generic plugins. This class defines options and handling that should be common to generic plugins. These plugins target the OpenStack identity service however are designed to be independent of API version. It provides the options expected by the :py:class:`keystoneauth1.identity.v3.BaseGenericPlugin` class. """ def get_options(self): options = super(BaseGenericLoader, self).get_options() options.extend([ opts.Opt('domain-id', help='Domain ID to scope to'), opts.Opt('domain-name', help='Domain name to scope to'), opts.Opt('project-id', help='Project ID to scope to', deprecated=[opts.Opt('tenant-id')]), opts.Opt('project-name', help='Project name to scope to', deprecated=[opts.Opt('tenant-name')]), opts.Opt('project-domain-id', help='Domain ID containing project'), opts.Opt('project-domain-name', help='Domain name containing project'), opts.Opt('trust-id', help='Trust ID'), opts.Opt('default-domain-id', help='Optional domain ID to use with v3 and v2 ' 'parameters. It will be used for both the user ' 'and project domain in v3 and ignored in ' 'v2 authentication.'), opts.Opt('default-domain-name', help='Optional domain name to use with v3 API and v2 ' 'parameters. It will be used for both the user ' 'and project domain in v3 and ignored in ' 'v2 authentication.'), ]) return options keystoneauth-2.4.1/keystoneauth1/loading/opts.py000066400000000000000000000123241271245473000220300ustar00rootroot00000000000000# 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 itertools import os from positional import positional try: from oslo_config import cfg except ImportError: cfg = None __all__ = ('Opt',) class Opt(object): """An option required by an authentication plugin. Opts provide a means for authentication plugins that are going to be dynamically loaded to specify the parameters that are to be passed to the plugin on initialization. The Opt specifies information about the way the plugin parameter is to be represented in different loading mechanisms. When defining an Opt with a - the - should be present in the name parameter. This will automatically be converted to an _ when passing to the plugin initialization. For example, you should specify:: Opt('user-domain-id') which will pass the value as `user_domain_id` to the plugin's initialization. :param str name: The name of the option. :param callable type: The type of the option. This is a callable which is passed the raw option that was loaded (often a string) and is required to return the parameter in the type expected by __init__. :param str help: The help text that is shown along with the option. :param bool secret: If the parameter is secret it should not be printed or logged in debug output. :param str dest: the name of the argument that will be passed to __init__. This allows you to have a different name in loading than is used by the __init__ function. Defaults to the the value of name. :param opt: A list of other options that are deprecated in favour of this one. This ensures the old options are still registered. :type opt: list(Opt) :param default: A default value that can be used if one is not provided. :param str metavar: The that should be printed in CLI help text. :param bool required: If the option is required to load the plugin. If a required option is not present loading should fail. """ @positional() def __init__(self, name, type=str, help=None, secret=False, dest=None, deprecated=None, default=None, metavar=None, required=False): if not callable(type): raise TypeError('type must be callable') if dest is None: dest = name.replace('-', '_') self.name = name self.type = type self.help = help self.secret = secret self.required = required self.dest = dest self.deprecated = [] if deprecated is None else deprecated self.default = default self.metavar = metavar # These are for oslo.config compat self.deprecated_opts = self.deprecated self.deprecated_for_removal = [] self.sample_default = None self.group = None def __repr__(self): return '' % self.name def _to_oslo_opt(self): if not cfg: raise ImportError("oslo.config is not an automatic dependency of " "keystoneauth. If you wish to use oslo.config " "you need to import it into your application's " "requirements file. ") deprecated_opts = [cfg.DeprecatedOpt(o.name) for o in self.deprecated] return cfg.Opt(name=self.name, type=self.type, help=self.help, secret=self.secret, required=self.required, dest=self.dest, deprecated_opts=deprecated_opts, metavar=self.metavar) def __eq__(self, other): return (type(self) == type(other) and self.name == other.name and self.type == other.type and self.help == other.help and self.secret == other.secret and self.required == other.required and self.dest == other.dest and self.deprecated == other.deprecated and self.default == other.default and self.metavar == other.metavar) @property def _all_opts(self): return itertools.chain([self], self.deprecated) @property def argparse_args(self): return ['--os-%s' % o.name for o in self._all_opts] @property def argparse_default(self): # select the first ENV that is not false-y or return None for o in self._all_opts: v = os.environ.get('OS_%s' % o.name.replace('-', '_').upper()) if v: return v return self.default keystoneauth-2.4.1/keystoneauth1/loading/session.py000066400000000000000000000234151271245473000225310ustar00rootroot00000000000000# 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 argparse import os from positional import positional try: from oslo_config import cfg except ImportError: cfg = None from keystoneauth1.loading import base from keystoneauth1 import session __all__ = ('register_argparse_arguments', 'load_from_argparse_arguments', 'register_conf_options', 'load_from_conf_options', 'get_conf_options') def _positive_non_zero_float(argument_value): if argument_value is None: return None try: value = float(argument_value) except ValueError: msg = "%s must be a float" % argument_value raise argparse.ArgumentTypeError(msg) if value <= 0: msg = "%s must be greater than 0" % argument_value raise argparse.ArgumentTypeError(msg) return value class Session(base.BaseLoader): @property def plugin_class(self): return session.Session def get_options(self): return [] @positional(1) def load_from_options(self, insecure=False, verify=None, cacert=None, cert=None, key=None, **kwargs): """Create a session with individual certificate parameters. Some parameters used to create a session don't lend themselves to be loaded from config/CLI etc. Create a session by converting those parameters into session __init__ parameters. """ if verify is None: if insecure: verify = False else: verify = cacert or True if cert and key: # passing cert and key together is deprecated in favour of the # requests lib form of having the cert and key as a tuple cert = (cert, key) return super(Session, self).load_from_options(verify=verify, cert=cert, **kwargs) def register_argparse_arguments(self, parser): session_group = parser.add_argument_group( 'API Connection Options', 'Options controlling the HTTP API Connections') session_group.add_argument( '--insecure', default=False, action='store_true', help='Explicitly allow client to perform ' '"insecure" TLS (https) requests. The ' 'server\'s certificate will not be verified ' 'against any certificate authorities. This ' 'option should be used with caution.') session_group.add_argument( '--os-cacert', metavar='', default=os.environ.get('OS_CACERT'), help='Specify a CA bundle file to use in ' 'verifying a TLS (https) server certificate. ' 'Defaults to env[OS_CACERT].') session_group.add_argument( '--os-cert', metavar='', default=os.environ.get('OS_CERT'), help='Defaults to env[OS_CERT].') session_group.add_argument( '--os-key', metavar='', default=os.environ.get('OS_KEY'), help='Defaults to env[OS_KEY].') session_group.add_argument( '--timeout', default=600, type=_positive_non_zero_float, metavar='', help='Set request timeout (in seconds).') def load_from_argparse_arguments(self, namespace, **kwargs): kwargs.setdefault('insecure', namespace.insecure) kwargs.setdefault('cacert', namespace.os_cacert) kwargs.setdefault('cert', namespace.os_cert) kwargs.setdefault('key', namespace.os_key) kwargs.setdefault('timeout', namespace.timeout) return self.load_from_options(**kwargs) def get_conf_options(self, deprecated_opts=None): """Get oslo_config options that are needed for a :py:class:`.Session`. These may be useful without being registered for config file generation or to manipulate the options before registering them yourself. The options that are set are: :cafile: The certificate authority filename. :certfile: The client certificate file to present. :keyfile: The key for the client certificate. :insecure: Whether to ignore SSL verification. :timeout: The max time to wait for HTTP connections. :param dict deprecated_opts: Deprecated options that should be included in the definition of new options. This should be a dict from the name of the new option to a list of oslo.DeprecatedOpts that correspond to the new option. (optional) For example, to support the ``ca_file`` option pointing to the new ``cafile`` option name:: old_opt = oslo_cfg.DeprecatedOpt('ca_file', 'old_group') deprecated_opts={'cafile': [old_opt]} :returns: A list of oslo_config options. """ if not cfg: raise ImportError("oslo.config is not an automatic dependency of " "keystoneauth. If you wish to use oslo.config " "you need to import it into your application's " "requirements file. ") if deprecated_opts is None: deprecated_opts = {} return [cfg.StrOpt('cafile', deprecated_opts=deprecated_opts.get('cafile'), help='PEM encoded Certificate Authority to use ' 'when verifying HTTPs connections.'), cfg.StrOpt('certfile', deprecated_opts=deprecated_opts.get('certfile'), help='PEM encoded client certificate cert file'), cfg.StrOpt('keyfile', deprecated_opts=deprecated_opts.get('keyfile'), help='PEM encoded client certificate key file'), cfg.BoolOpt('insecure', default=False, deprecated_opts=deprecated_opts.get('insecure'), help='Verify HTTPS connections.'), cfg.IntOpt('timeout', deprecated_opts=deprecated_opts.get('timeout'), help='Timeout value for http requests'), ] def register_conf_options(self, conf, group, deprecated_opts=None): """Register the oslo_config options that are needed for a session. The options that are set are: :cafile: The certificate authority filename. :certfile: The client certificate file to present. :keyfile: The key for the client certificate. :insecure: Whether to ignore SSL verification. :timeout: The max time to wait for HTTP connections. :param oslo_config.Cfg conf: config object to register with. :param string group: The ini group to register options in. :param dict deprecated_opts: Deprecated options that should be included in the definition of new options. This should be a dict from the name of the new option to a list of oslo.DeprecatedOpts that correspond to the new option. (optional) For example, to support the ``ca_file`` option pointing to the new ``cafile`` option name:: old_opt = oslo_cfg.DeprecatedOpt('ca_file', 'old_group') deprecated_opts={'cafile': [old_opt]} :returns: The list of options that was registered. """ opts = self.get_conf_options(deprecated_opts=deprecated_opts) conf.register_group(cfg.OptGroup(group)) conf.register_opts(opts, group=group) return opts def load_from_conf_options(self, conf, group, **kwargs): """Create a session object from an oslo_config object. The options must have been previously registered with register_conf_options. :param oslo_config.Cfg conf: config object to register with. :param string group: The ini group to register options in. :param dict kwargs: Additional parameters to pass to session construction. :returns: A new session object. :rtype: :py:class:`.Session` """ c = conf[group] kwargs.setdefault('insecure', c.insecure) kwargs.setdefault('cacert', c.cafile) kwargs.setdefault('cert', c.certfile) kwargs.setdefault('key', c.keyfile) kwargs.setdefault('timeout', c.timeout) return self.load_from_options(**kwargs) def register_argparse_arguments(*args, **kwargs): return Session().register_argparse_arguments(*args, **kwargs) def load_from_argparse_arguments(*args, **kwargs): return Session().load_from_argparse_arguments(*args, **kwargs) def register_conf_options(*args, **kwargs): return Session().register_conf_options(*args, **kwargs) def load_from_conf_options(*args, **kwargs): return Session().load_from_conf_options(*args, **kwargs) def get_conf_options(*args, **kwargs): return Session().get_conf_options(*args, **kwargs) keystoneauth-2.4.1/keystoneauth1/plugin.py000066400000000000000000000225311271245473000207250ustar00rootroot00000000000000# 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(jamielennox): The AUTH_INTERFACE is a special value that can be # requested from get_endpoint. If a plugin receives this as the value of # 'interface' it should return the initial URL that was passed to the plugin. AUTH_INTERFACE = object() IDENTITY_AUTH_HEADER_NAME = 'X-Auth-Token' class BaseAuthPlugin(object): """The basic structure of an authentication plugin.""" def get_token(self, session, **kwargs): """Obtain a token. How the token is obtained is up to the plugin. If it is still valid it may be re-used, retrieved from cache or invoke an authentication request against a server. There are no required kwargs. They are passed directly to the auth plugin and they are implementation specific. Returning None will indicate that no token was able to be retrieved. This function is misplaced as it should only be required for auth plugins that use the 'X-Auth-Token' header. However due to the way plugins evolved this method is required and often called to trigger an authentication request on a new plugin. When implementing a new plugin it is advised that you implement this method, however if you don't require the 'X-Auth-Token' header override the `get_headers` method instead. :param session: A session object so the plugin can make HTTP calls. :type session: keystoneauth1.session.Session :return: A token to use. :rtype: string """ return None def get_headers(self, session, **kwargs): """Fetch authentication headers for message. This is a more generalized replacement of the older get_token to allow plugins to specify different or additional authentication headers to the OpenStack standard 'X-Auth-Token' header. How the authentication headers are obtained is up to the plugin. If the headers are still valid they may be re-used, retrieved from cache or the plugin may invoke an authentication request against a server. The default implementation of get_headers calls the `get_token` method to enable older style plugins to continue functioning unchanged. Subclasses should feel free to completely override this function to provide the headers that they want. There are no required kwargs. They are passed directly to the auth plugin and they are implementation specific. Returning None will indicate that no token was able to be retrieved and that authorization was a failure. Adding no authentication data can be achieved by returning an empty dictionary. :param session: The session object that the auth_plugin belongs to. :type session: keystoneauth1.session.Session :returns: Headers that are set to authenticate a message or None for failure. Note that when checking this value that the empty dict is a valid, non-failure response. :rtype: dict """ token = self.get_token(session) if not token: return None return {IDENTITY_AUTH_HEADER_NAME: token} def get_endpoint(self, session, **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. However there are certain standard options that will be generated by the clients and should be used by plugins: - ``service_type``: what sort of service is required. - ``service_name``: the name of the service in the catalog. - ``interface``: what visibility the endpoint should have. - ``region_name``: the region the endpoint exists in. :param session: The session object that the auth_plugin belongs to. :type session: keystoneauth1.session.Session :returns: The base URL that will be used to talk to the required service or None if not available. :rtype: string """ return None def get_connection_params(self, session, **kwargs): """Return any additional connection parameters required for the plugin. :param session: The session object that the auth_plugin belongs to. :type session: keystoneclient.session.Session :returns: Headers that are set to authenticate a message or None for failure. Note that when checking this value that the empty dict is a valid, non-failure response. :rtype: dict """ return {} def invalidate(self): """Invalidate the current authentication data. This should result in fetching a new token on next call. A plugin may be invalidated if an Unauthorized HTTP response is returned to indicate that the token may have been revoked or is otherwise now invalid. :returns: True if there was something that the plugin did to invalidate. This means that it makes sense to try again. If nothing happens returns False to indicate give up. :rtype: bool """ return False def get_user_id(self, session, **kwargs): """Return a unique user identifier of the plugin. Wherever possible the user id should be inferred from the token however there are certain URLs and other places that require access to the currently authenticated user id. :param session: A session object so the plugin can make HTTP calls. :type session: keystoneauth1.session.Session :returns: A user identifier or None if one is not available. :rtype: str """ return None def get_project_id(self, session, **kwargs): """Return the project id that we are authenticated to. Wherever possible the project id should be inferred from the token however there are certain URLs and other places that require access to the currently authenticated project id. :param session: A session object so the plugin can make HTTP calls. :type session: keystoneauth1.session.Session :returns: A project identifier or None if one is not available. :rtype: str """ return None def get_sp_auth_url(self, session, sp_id, **kwargs): """Return auth_url from the Service Provider object This url is used for obtaining unscoped federated token from remote cloud. :param sp_id: ID of the Service Provider to be queried. :type sp_id: string :returns: A Service Provider auth_url or None if one is not available. :rtype: str """ return None def get_sp_url(self, session, sp_id, **kwargs): """Return sp_url from the Service Provider object This url is used for passing SAML2 assertion to the remote cloud. :param sp_id: ID of the Service Provider to be queried. :type sp_id: str :returns: A Service Provider sp_url or None if one is not available. :rtype: str """ return None def get_cache_id(self): """Fetch an identifier that uniquely identifies the auth options. The returned identifier need not be decomposable or otherwise provide anyway to recreate the plugin. It should not contain sensitive data in plaintext. This string MUST change if any of the parameters that are used to uniquely identity this plugin change. If get_cache_id returns a str value suggesting that caching is supported then get_auth_cache and set_auth_cache must also be implemented. :returns: A unique string for the set of options :rtype: str or None if this is unsupported or unavailable. """ return None def get_auth_state(self): """Retrieve the current authentication state for the plugin. Retrieve any internal state that represents the authenticated plugin. This should not fetch any new data if it is not present. :raises NotImplementedError: if the plugin does not support this feature. :returns: raw python data (which can be JSON serialized) that can be moved into another plugin (of the same type) to have the same authenticated state. :rtype: object or None if unauthenticated. """ raise NotImplementedError() def set_auth_state(self, data): """Install existing authentication state for a plugin. Take the output of get_auth_state and install that authentication state into the current authentication plugin. :raises NotImplementedError: if the plugin does not support this feature. """ raise NotImplementedError() keystoneauth-2.4.1/keystoneauth1/session.py000066400000000000000000001005621271245473000211130ustar00rootroot00000000000000# 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 functools import hashlib import json import logging import platform import socket import time import uuid from positional import positional import requests import six from six.moves import urllib import keystoneauth1 from keystoneauth1 import _utils as utils from keystoneauth1 import exceptions try: import netaddr except ImportError: netaddr = None try: import osprofiler.web as osprofiler_web except ImportError: osprofiler_web = None DEFAULT_USER_AGENT = 'keystoneauth1/%s %s %s/%s' % ( keystoneauth1.__version__, requests.utils.default_user_agent(), platform.python_implementation(), platform.python_version()) _logger = utils.get_logger(__name__) def _construct_session(session_obj=None): # NOTE(morganfainberg): if the logic in this function changes be sure to # update the betamax fixture's '_construct_session_with_betamax" function # as well. if not session_obj: session_obj = requests.Session() # Use TCPKeepAliveAdapter to fix bug 1323862 for scheme in list(session_obj.adapters): session_obj.mount(scheme, TCPKeepAliveAdapter()) return session_obj class _JSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, datetime.datetime): return o.isoformat() if isinstance(o, uuid.UUID): return six.text_type(o) if netaddr and isinstance(o, netaddr.IPAddress): return six.text_type(o) return super(_JSONEncoder, self).default(o) class _StringFormatter(object): """A String formatter that fetches values on demand""" def __init__(self, session, auth): self.session = session self.auth = auth def __getitem__(self, item): if item == 'project_id': value = self.session.get_project_id(self.auth) elif item == 'user_id': value = self.session.get_user_id(self.auth) else: raise AttributeError(item) if not value: raise ValueError("This type of authentication does not provide a " "%s that can be substituted" % item) return value class Session(object): """Maintains client communication state and common functionality. As much as possible the parameters to this class reflect and are passed directly to the requests library. :param auth: An authentication plugin to authenticate the session with. (optional, defaults to None) :type auth: :py:class:`keystonauth.auth.base.BaseAuthPlugin` :param requests.Session session: A requests session object that can be used for issuing requests. (optional) :param string original_ip: The original IP of the requesting user which will be sent to identity service in a 'Forwarded' header. (optional) :param verify: The verification arguments to pass to requests. These are of the same form as requests expects, so True or False to verify (or not) against system certificates or a path to a bundle or CA certs to check against or None for requests to attempt to locate and use certificates. (optional, defaults to True) :param cert: A client certificate to pass to requests. These are of the same form as requests expects. Either a single filename containing both the certificate and key or a tuple containing the path to the certificate then a path to the key. (optional) :param float timeout: A timeout to pass to requests. This should be a numerical value indicating some amount (or fraction) of seconds or 0 for no timeout. (optional, defaults to 0) :param string user_agent: A User-Agent header string to use for the request. If not provided, a default of :attr:`~keystoneauth1.session.DEFAULT_USER_AGENT` is used, which contains the keystoneauth1 version as well as those of the requests library and which Python is being used. When a non-None value is passed, it will be prepended to the default. :param int/bool redirect: Controls the maximum number of redirections that can be followed by a request. Either an integer for a specific count or True/False for forever/never. (optional, default to 30) """ user_agent = None _REDIRECT_STATUSES = (301, 302, 303, 305, 307) _DEFAULT_REDIRECT_LIMIT = 30 @positional(2) def __init__(self, auth=None, session=None, original_ip=None, verify=True, cert=None, timeout=None, user_agent=None, redirect=_DEFAULT_REDIRECT_LIMIT): self.auth = auth self.session = _construct_session(session) self.original_ip = original_ip self.verify = verify self.cert = cert self.timeout = None self.redirect = redirect if timeout is not None: self.timeout = float(timeout) # don't override the class variable if none provided if user_agent is not None: # Per RFC 7231 Section 5.5.3, identifiers in a user-agent # should be ordered by decreasing significance. # If a user sets their product, we prepend it to the KSA # version, requests version, and then the Python version. self.user_agent = "%s %s" % (user_agent, DEFAULT_USER_AGENT) self._json = _JSONEncoder() @property def adapters(self): return self.session.adapters @adapters.setter def adapters(self, value): self.session.adapters = value def mount(self, scheme, adapter): self.session.mount(scheme, adapter) def _remove_service_catalog(self, body): try: data = json.loads(body) # V3 token if 'token' in data and 'catalog' in data['token']: data['token']['catalog'] = '' return self._json.encode(data) # V2 token if 'serviceCatalog' in data['access']: data['access']['serviceCatalog'] = '' return self._json.encode(data) except Exception: # Don't fail trying to clean up the request body. pass return body @staticmethod def _process_header(header): """Redacts the secure headers to be logged.""" secure_headers = ('authorization', 'x-auth-token', 'x-subject-token',) if header[0].lower() in secure_headers: token_hasher = hashlib.sha1() token_hasher.update(header[1].encode('utf-8')) token_hash = token_hasher.hexdigest() return (header[0], '{SHA1}%s' % token_hash) return header @positional() def _http_log_request(self, url, method=None, data=None, json=None, headers=None, logger=_logger): if not logger.isEnabledFor(logging.DEBUG): # NOTE(morganfainberg): This whole debug section is expensive, # there is no need to do the work if we're not going to emit a # debug log. return string_parts = ['REQ: curl -g -i'] # NOTE(jamielennox): None means let requests do its default validation # so we need to actually check that this is False. if self.verify is False: string_parts.append('--insecure') elif isinstance(self.verify, six.string_types): string_parts.append('--cacert "%s"' % self.verify) if method: string_parts.extend(['-X', method]) string_parts.append(url) if headers: for header in six.iteritems(headers): string_parts.append('-H "%s: %s"' % self._process_header(header)) if json: data = self._json.encode(json) if data: if isinstance(data, six.binary_type): try: data = data.decode("ascii") except UnicodeDecodeError: data = "" string_parts.append("-d '%s'" % data) logger.debug(' '.join(string_parts)) @positional() def _http_log_response(self, response=None, json=None, status_code=None, headers=None, text=None, logger=_logger): if not logger.isEnabledFor(logging.DEBUG): return if response is not None: if not status_code: status_code = response.status_code if not headers: headers = response.headers if not text: text = self._remove_service_catalog(response.text) if json: text = self._json.encode(json) string_parts = ['RESP:'] if status_code: string_parts.append('[%s]' % status_code) if headers: for header in six.iteritems(headers): string_parts.append('%s: %s' % self._process_header(header)) if text: string_parts.append('\nRESP BODY: %s\n' % text) logger.debug(' '.join(string_parts)) @positional() def request(self, url, method, json=None, original_ip=None, user_agent=None, redirect=None, authenticated=None, endpoint_filter=None, auth=None, requests_auth=None, raise_exc=True, allow_reauth=True, log=True, endpoint_override=None, connect_retries=0, logger=_logger, **kwargs): """Send an HTTP request with the specified characteristics. Wrapper around `requests.Session.request` to handle tasks such as setting headers, JSON encoding/decoding, and error handling. Arguments that are not handled are passed through to the requests library. :param string url: Path or fully qualified URL of HTTP request. If only a path is provided then endpoint_filter must also be provided such that the base URL can be determined. If a fully qualified URL is provided then endpoint_filter will be ignored. :param string method: The http method to use. (e.g. 'GET', 'POST') :param string original_ip: Mark this request as forwarded for this ip. (optional) :param dict headers: Headers to be included in the request. (optional) :param json: Some data to be represented as JSON. (optional) :param string user_agent: A user_agent to use for the request. If present will override one present in headers. (optional) :param int/bool redirect: the maximum number of redirections that can be followed by a request. Either an integer for a specific count or True/False for forever/never. (optional) :param int connect_retries: the maximum number of retries that should be attempted for connection errors. (optional, defaults to 0 - never retry). :param bool authenticated: True if a token should be attached to this request, False if not or None for attach if an auth_plugin is available. (optional, defaults to None) :param dict endpoint_filter: Data to be provided to an auth plugin with which it should be able to determine an endpoint to use for this request. If not provided then URL is expected to be a fully qualified URL. (optional) :param str endpoint_override: The URL to use instead of looking up the endpoint in the auth plugin. This will be ignored if a fully qualified URL is provided but take priority over an endpoint_filter. This string may contain the values %(project_id)s and %(user_id)s to have those values replaced by the project_id/user_id of the current authentication. (optional) :param auth: The auth plugin to use when authenticating this request. This will override the plugin that is attached to the session (if any). (optional) :type auth: :py:class:`keystonauth.auth.base.BaseAuthPlugin` :param requests_auth: A requests library auth plugin that cannot be passed via kwarg because the `auth` kwarg collides with our own auth plugins. (optional) :type requests_auth: :py:class:`requests.auth.AuthBase` :param bool raise_exc: If True then raise an appropriate exception for failed HTTP requests. If False then return the request object. (optional, default True) :param bool allow_reauth: Allow fetching a new token and retrying the request on receiving a 401 Unauthorized response. (optional, default True) :param bool log: If True then log the request and response data to the debug log. (optional, default True) :param logger: The logger object to use to log request and responses. If not provided the keystonauth.session default logger will be used. :type logger: logging.Logger :param kwargs: any other parameter that can be passed to requests.Session.request (such as `headers`). Except: 'data' will be overwritten by the data in 'json' param. 'allow_redirects' is ignored as redirects are handled by the session. :raises keystonauth.exceptions.ClientException: For connection failure, or to indicate an error response code. :returns: The response to the request. """ headers = kwargs.setdefault('headers', dict()) if authenticated is None: authenticated = bool(auth or self.auth) if authenticated: auth_headers = self.get_auth_headers(auth) if auth_headers is None: msg = 'No valid authentication is available' raise exceptions.AuthorizationFailure(msg) headers.update(auth_headers) if osprofiler_web: headers.update(osprofiler_web.get_trace_id_headers()) # if we are passed a fully qualified URL and an endpoint_filter we # should ignore the filter. This will make it easier for clients who # want to overrule the default endpoint_filter data added to all client # requests. We check fully qualified here by the presence of a host. if not urllib.parse.urlparse(url).netloc: base_url = None if endpoint_override: base_url = endpoint_override % _StringFormatter(self, auth) elif endpoint_filter: base_url = self.get_endpoint(auth, **endpoint_filter) if not base_url: raise exceptions.EndpointNotFound() url = '%s/%s' % (base_url.rstrip('/'), url.lstrip('/')) if self.cert: kwargs.setdefault('cert', self.cert) if self.timeout is not None: kwargs.setdefault('timeout', self.timeout) if user_agent: headers['User-Agent'] = user_agent elif self.user_agent: user_agent = headers.setdefault('User-Agent', self.user_agent) else: user_agent = headers.setdefault('User-Agent', DEFAULT_USER_AGENT) if self.original_ip: headers.setdefault('Forwarded', 'for=%s;by=%s' % (self.original_ip, user_agent)) if json is not None: headers['Content-Type'] = 'application/json' kwargs['data'] = self._json.encode(json) kwargs.setdefault('verify', self.verify) if requests_auth: kwargs['auth'] = requests_auth if log: self._http_log_request(url, method=method, data=kwargs.get('data'), headers=headers, logger=logger) # Force disable requests redirect handling. We will manage this below. kwargs['allow_redirects'] = False if redirect is None: redirect = self.redirect send = functools.partial(self._send_request, url, method, redirect, log, logger, connect_retries) try: connection_params = self.get_auth_connection_params(auth=auth) except exceptions.MissingAuthPlugin: # NOTE(jamielennox): If we've gotten this far without an auth # plugin then we should be happy with allowing no additional # connection params. This will be the typical case for plugins # anyway. pass else: if connection_params: kwargs.update(connection_params) resp = send(**kwargs) # handle getting a 401 Unauthorized response by invalidating the plugin # and then retrying the request. This is only tried once. if resp.status_code == 401 and authenticated and allow_reauth: if self.invalidate(auth): auth_headers = self.get_auth_headers(auth) if auth_headers is not None: headers.update(auth_headers) resp = send(**kwargs) if raise_exc and resp.status_code >= 400: logger.debug('Request returned failure status: %s', resp.status_code) raise exceptions.from_response(resp, method, url) return resp def _send_request(self, url, method, redirect, log, logger, connect_retries, connect_retry_delay=0.5, **kwargs): # NOTE(jamielennox): We handle redirection manually because the # requests lib follows some browser patterns where it will redirect # POSTs as GETs for certain statuses which is not want we want for an # API. See: https://en.wikipedia.org/wiki/Post/Redirect/Get # NOTE(jamielennox): The interaction between retries and redirects are # handled naively. We will attempt only a maximum number of retries and # redirects rather than per request limits. Otherwise the extreme case # could be redirects * retries requests. This will be sufficient in # most cases and can be fixed properly if there's ever a need. try: try: resp = self.session.request(method, url, **kwargs) except requests.exceptions.SSLError as e: msg = 'SSL exception connecting to %(url)s: %(error)s' % { 'url': url, 'error': e} raise exceptions.SSLError(msg) except requests.exceptions.Timeout: msg = 'Request to %s timed out' % url raise exceptions.ConnectTimeout(msg) except requests.exceptions.ConnectionError: msg = 'Unable to establish connection to %s' % url raise exceptions.ConnectFailure(msg) except requests.exceptions.RequestException as e: msg = 'Unexpected exception for %(url)s: %(error)s' % { 'url': url, 'error': e} raise exceptions.UnknownConnectionError(msg, e) except exceptions.RetriableConnectionFailure as e: if connect_retries <= 0: raise logger.info('Failure: %(e)s. Retrying in %(delay).1fs.', {'e': e, 'delay': connect_retry_delay}) time.sleep(connect_retry_delay) return self._send_request( url, method, redirect, log, logger, connect_retries=connect_retries - 1, connect_retry_delay=connect_retry_delay * 2, **kwargs) if log: self._http_log_response(response=resp, logger=logger) if resp.status_code in self._REDIRECT_STATUSES: # be careful here in python True == 1 and False == 0 if isinstance(redirect, bool): redirect_allowed = redirect else: redirect -= 1 redirect_allowed = redirect >= 0 if not redirect_allowed: return resp try: location = resp.headers['location'] except KeyError: logger.warning("Failed to redirect request to %s as new " "location was not provided.", resp.url) else: # NOTE(jamielennox): We don't pass through connect_retry_delay. # This request actually worked so we can reset the delay count. new_resp = self._send_request( location, method, redirect, log, logger, connect_retries=connect_retries, **kwargs) if not isinstance(new_resp.history, list): new_resp.history = list(new_resp.history) new_resp.history.insert(0, resp) resp = new_resp return resp def head(self, url, **kwargs): """Perform a HEAD request. This calls :py:meth:`.request()` with ``method`` set to ``HEAD``. """ return self.request(url, 'HEAD', **kwargs) def get(self, url, **kwargs): """Perform a GET request. This calls :py:meth:`.request()` with ``method`` set to ``GET``. """ return self.request(url, 'GET', **kwargs) def post(self, url, **kwargs): """Perform a POST request. This calls :py:meth:`.request()` with ``method`` set to ``POST``. """ return self.request(url, 'POST', **kwargs) def put(self, url, **kwargs): """Perform a PUT request. This calls :py:meth:`.request()` with ``method`` set to ``PUT``. """ return self.request(url, 'PUT', **kwargs) def delete(self, url, **kwargs): """Perform a DELETE request. This calls :py:meth:`.request()` with ``method`` set to ``DELETE``. """ return self.request(url, 'DELETE', **kwargs) def patch(self, url, **kwargs): """Perform a PATCH request. This calls :py:meth:`.request()` with ``method`` set to ``PATCH``. """ return self.request(url, 'PATCH', **kwargs) def _auth_required(self, auth, msg): if not auth: auth = self.auth if not auth: msg_fmt = 'An auth plugin is required to %s' raise exceptions.MissingAuthPlugin(msg_fmt % msg) return auth def get_auth_headers(self, auth=None, **kwargs): """Return auth headers as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: :py:class:`keystonauth.auth.base.BaseAuthPlugin` :raises keystonauth.exceptions.AuthorizationFailure: if a new token fetch fails. :raises keystonauth.exceptions.MissingAuthPlugin: if a plugin is not available. :returns: Authentication headers or None for failure. :rtype: dict """ auth = self._auth_required(auth, 'fetch a token') return auth.get_headers(self, **kwargs) def get_token(self, auth=None): """Return a token as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: :py:class:`keystonauth.auth.base.BaseAuthPlugin` :raises keystonauth.exceptions.AuthorizationFailure: if a new token fetch fails. :raises keystonauth.exceptions.MissingAuthPlugin: if a plugin is not available. *DEPRECATED*: This assumes that the only header that is used to authenticate a message is 'X-Auth-Token'. This may not be correct. Use get_auth_headers instead. :returns: A valid token. :rtype: string """ return (self.get_auth_headers(auth) or {}).get('X-Auth-Token') def get_endpoint(self, auth=None, **kwargs): """Get an endpoint as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: :py:class:`keystonauth.auth.base.BaseAuthPlugin` :raises keystonauth.exceptions.MissingAuthPlugin: if a plugin is not available. :returns: An endpoint if available or None. :rtype: string """ auth = self._auth_required(auth, 'determine endpoint URL') return auth.get_endpoint(self, **kwargs) def get_auth_connection_params(self, auth=None, **kwargs): """Return auth connection params as provided by the auth plugin. An auth plugin may specify connection parameters to the request like providing a client certificate for communication. We restrict the values that may be returned from this function to prevent an auth plugin overriding values unrelated to connection parmeters. The values that are currently accepted are: - `cert`: a path to a client certificate, or tuple of client certificate and key pair that are used with this request. - `verify`: a boolean value to indicate verifying SSL certificates against the system CAs or a path to a CA file to verify with. These values are passed to the requests library and further information on accepted values may be found there. :param auth: The auth plugin to use for tokens. Overrides the plugin on the session. (optional) :type auth: keystoneclient.auth.base.BaseAuthPlugin :raises keystoneclient.exceptions.AuthorizationFailure: if a new token fetch fails. :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not available. :raises keystoneclient.exceptions.UnsupportedParameters: if the plugin returns a parameter that is not supported by this session. :returns: Authentication headers or None for failure. :rtype: dict """ msg = 'An auth plugin is required to fetch connection params' auth = self._auth_required(auth, msg) params = auth.get_connection_params(self, **kwargs) # NOTE(jamielennox): There needs to be some consensus on what # parameters are allowed to be modified by the auth plugin here. # Ideally I think it would be only the send() parts of the request # flow. For now lets just allow certain elements. params_copy = params.copy() for arg in ('cert', 'verify'): try: kwargs[arg] = params_copy.pop(arg) except KeyError: pass if params_copy: raise exceptions.UnsupportedParameters(list(params_copy.keys())) return params def invalidate(self, auth=None): """Invalidate an authentication plugin. :param auth: The auth plugin to invalidate. Overrides the plugin on the session. (optional) :type auth: :py:class:`keystonauth.auth.base.BaseAuthPlugin` """ auth = self._auth_required(auth, 'validate') return auth.invalidate() def get_user_id(self, auth=None): """Return the authenticated user_id as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystonauth.auth.base.BaseAuthPlugin :raises keystonauth.exceptions.AuthorizationFailure: if a new token fetch fails. :raises keystonauth.exceptions.MissingAuthPlugin: if a plugin is not available. :returns string: Current user_id or None if not supported by plugin. """ auth = self._auth_required(auth, 'get user_id') return auth.get_user_id(self) def get_project_id(self, auth=None): """Return the authenticated project_id as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystonauth.auth.base.BaseAuthPlugin :raises keystonauth.exceptions.AuthorizationFailure: if a new token fetch fails. :raises keystonauth.exceptions.MissingAuthPlugin: if a plugin is not available. :returns string: Current project_id or None if not supported by plugin. """ auth = self._auth_required(auth, 'get project_id') return auth.get_project_id(self) REQUESTS_VERSION = tuple(int(v) for v in requests.__version__.split('.')) class TCPKeepAliveAdapter(requests.adapters.HTTPAdapter): """The custom adapter used to set TCP Keep-Alive on all connections. This Adapter also preserves the default behaviour of Requests which disables Nagle's Algorithm. See also: http://blogs.msdn.com/b/windowsazurestorage/archive/2010/06/25/nagle-s-algorithm-is-not-friendly-towards-small-requests.aspx """ def init_poolmanager(self, *args, **kwargs): if 'socket_options' not in kwargs and REQUESTS_VERSION >= (2, 4, 1): socket_options = [ # Keep Nagle's algorithm off (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), # Turn on TCP Keep-Alive (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), ] # Some operating systems (e.g., OSX) do not support setting # keepidle if hasattr(socket, 'TCP_KEEPIDLE'): socket_options += [ # Wait 60 seconds before sending keep-alive probes (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) ] if hasattr(socket, 'TCP_KEEPCNT'): socket_options += [ # Set the maximum number of keep-alive probes (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4), ] if hasattr(socket, 'TCP_KEEPINTVL'): socket_options += [ # Send keep-alive probes every 15 seconds (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15), ] # After waiting 60 seconds, and then sending a probe once every 15 # seconds 4 times, these options should ensure that a connection # hands for no longer than 2 minutes before a ConnectionError is # raised. kwargs['socket_options'] = socket_options super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs) keystoneauth-2.4.1/keystoneauth1/tests/000077500000000000000000000000001271245473000202145ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/__init__.py000066400000000000000000000000001271245473000223130ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/000077500000000000000000000000001271245473000211735ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/__init__.py000066400000000000000000000000001271245473000232720ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/access/000077500000000000000000000000001271245473000224345ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/access/__init__.py000066400000000000000000000000001271245473000245330ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/access/test_v2_access.py000066400000000000000000000172311271245473000257210ustar00rootroot00000000000000# 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 uuid from oslo_utils import timeutils from keystoneauth1 import access from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils class AccessV2Test(utils.TestCase): def test_building_unscoped_accessinfo(self): token = fixture.V2Token(expires='2012-10-03T16:58:01Z') auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertFalse(auth_ref.has_service_catalog()) self.assertEqual(auth_ref.auth_token, token.token_id) self.assertEqual(auth_ref.username, token.user_name) self.assertEqual(auth_ref.user_id, token.user_id) self.assertEqual(auth_ref.role_ids, []) self.assertEqual(auth_ref.role_names, []) self.assertIsNone(auth_ref.tenant_name) self.assertIsNone(auth_ref.tenant_id) self.assertFalse(auth_ref.domain_scoped) self.assertFalse(auth_ref.project_scoped) self.assertFalse(auth_ref.trust_scoped) self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertIsNone(auth_ref.user_domain_id) self.assertIsNone(auth_ref.user_domain_name) self.assertEqual(auth_ref.expires, token.expires) self.assertEqual(auth_ref.issued, token.issued) self.assertEqual(token.audit_id, auth_ref.audit_id) self.assertIsNone(auth_ref.audit_chain_id) self.assertIsNone(token.audit_chain_id) self.assertIsNone(auth_ref.bind) def test_will_expire_soon(self): token = fixture.V2Token() expires = timeutils.utcnow() + datetime.timedelta(minutes=5) token.expires = expires auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) self.assertTrue(auth_ref.will_expire_soon(stale_duration=300)) self.assertFalse(auth_ref.will_expire_soon()) def test_building_scoped_accessinfo(self): token = fixture.V2Token() token.set_scope() s = token.add_service('identity') s.add_endpoint('http://url') role_data = token.add_role() auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertTrue(auth_ref.has_service_catalog()) self.assertEqual(auth_ref.auth_token, token.token_id) self.assertEqual(auth_ref.username, token.user_name) self.assertEqual(auth_ref.user_id, token.user_id) self.assertEqual(auth_ref.role_ids, [role_data['id']]) self.assertEqual(auth_ref.role_names, [role_data['name']]) self.assertEqual(auth_ref.tenant_name, token.tenant_name) self.assertEqual(auth_ref.tenant_id, token.tenant_id) self.assertEqual(auth_ref.tenant_name, auth_ref.project_name) self.assertEqual(auth_ref.tenant_id, auth_ref.project_id) self.assertIsNone(auth_ref.project_domain_id, 'default') self.assertIsNone(auth_ref.project_domain_name, 'Default') self.assertIsNone(auth_ref.user_domain_id, 'default') self.assertIsNone(auth_ref.user_domain_name, 'Default') self.assertTrue(auth_ref.project_scoped) self.assertFalse(auth_ref.domain_scoped) self.assertEqual(token.audit_id, auth_ref.audit_id) self.assertEqual(token.audit_chain_id, auth_ref.audit_chain_id) def test_diablo_token(self): diablo_token = { 'access': { 'token': { 'id': uuid.uuid4().hex, 'expires': '2020-01-01T00:00:10.000123Z', 'tenantId': 'tenant_id1', }, 'user': { 'id': 'user_id1', 'name': 'user_name1', 'roles': [ {'name': 'role1'}, {'name': 'role2'}, ], }, }, } auth_ref = access.create(body=diablo_token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertTrue(auth_ref) self.assertEqual(auth_ref.username, 'user_name1') self.assertEqual(auth_ref.project_id, 'tenant_id1') self.assertEqual(auth_ref.project_name, 'tenant_id1') self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertIsNone(auth_ref.user_domain_id) self.assertIsNone(auth_ref.user_domain_name) self.assertEqual(auth_ref.role_names, ['role1', 'role2']) def test_grizzly_token(self): grizzly_token = { 'access': { 'token': { 'id': uuid.uuid4().hex, 'expires': '2020-01-01T00:00:10.000123Z', }, 'user': { 'id': 'user_id1', 'name': 'user_name1', 'tenantId': 'tenant_id1', 'tenantName': 'tenant_name1', 'roles': [ {'name': 'role1'}, {'name': 'role2'}, ], }, }, } auth_ref = access.create(body=grizzly_token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertEqual(auth_ref.project_id, 'tenant_id1') self.assertEqual(auth_ref.project_name, 'tenant_name1') self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertIsNone(auth_ref.user_domain_id, 'default') self.assertIsNone(auth_ref.user_domain_name, 'Default') self.assertEqual(auth_ref.role_names, ['role1', 'role2']) def test_v2_roles(self): role_id = 'a' role_name = 'b' token = fixture.V2Token() token.set_scope() token.add_role(id=role_id, name=role_name) auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertEqual([role_id], auth_ref.role_ids) self.assertEqual([role_id], auth_ref._data['access']['metadata']['roles']) self.assertEqual([role_name], auth_ref.role_names) self.assertEqual([{'name': role_name}], auth_ref._data['access']['user']['roles']) def test_trusts(self): user_id = uuid.uuid4().hex trust_id = uuid.uuid4().hex token = fixture.V2Token(user_id=user_id, trust_id=trust_id) token.set_scope() token.add_role() auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertEqual(trust_id, auth_ref.trust_id) self.assertEqual(user_id, auth_ref.trustee_user_id) self.assertEqual(trust_id, token['access']['trust']['id']) def test_binding(self): token = fixture.V2Token() principal = uuid.uuid4().hex token.set_bind('kerberos', principal) auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertEqual({'kerberos': principal}, auth_ref.bind) keystoneauth-2.4.1/keystoneauth1/tests/unit/access/test_v2_service_catalog.py000066400000000000000000000255421271245473000276160ustar00rootroot00000000000000# 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 uuid from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils class ServiceCatalogTest(utils.TestCase): def setUp(self): super(ServiceCatalogTest, self).setUp() self.AUTH_RESPONSE_BODY = fixture.V2Token( token_id='ab48a9efdfedb23ty3494', expires='2010-11-01T03:32:15-05:00', tenant_id='345', tenant_name='My Project', user_id='123', user_name='jqsmith', audit_chain_id=uuid.uuid4().hex) self.AUTH_RESPONSE_BODY.add_role(id='234', name='compute:admin') role = self.AUTH_RESPONSE_BODY.add_role(id='235', name='object-store:admin') role['tenantId'] = '1' s = self.AUTH_RESPONSE_BODY.add_service('compute', 'Cloud Servers') endpoint = s.add_endpoint( public='https://compute.north.host/v1/1234', internal='https://compute.north.host/v1/1234', region='North') endpoint['tenantId'] = '1' endpoint['versionId'] = '1.0' endpoint['versionInfo'] = 'https://compute.north.host/v1.0/' endpoint['versionList'] = 'https://compute.north.host/' endpoint = s.add_endpoint( public='https://compute.north.host/v1.1/3456', internal='https://compute.north.host/v1.1/3456', region='North') endpoint['tenantId'] = '2' endpoint['versionId'] = '1.1' endpoint['versionInfo'] = 'https://compute.north.host/v1.1/' endpoint['versionList'] = 'https://compute.north.host/' s = self.AUTH_RESPONSE_BODY.add_service('object-store', 'Cloud Files') endpoint = s.add_endpoint(public='https://swift.north.host/v1/blah', internal='https://swift.north.host/v1/blah', region='South') endpoint['tenantId'] = '11' endpoint['versionId'] = '1.0' endpoint['versionInfo'] = 'uri' endpoint['versionList'] = 'uri' endpoint = s.add_endpoint( public='https://swift.north.host/v1.1/blah', internal='https://compute.north.host/v1.1/blah', region='South') endpoint['tenantId'] = '2' endpoint['versionId'] = '1.1' endpoint['versionInfo'] = 'https://swift.north.host/v1.1/' endpoint['versionList'] = 'https://swift.north.host/' s = self.AUTH_RESPONSE_BODY.add_service('image', 'Image Servers') s.add_endpoint(public='https://image.north.host/v1/', internal='https://image-internal.north.host/v1/', region='North') s.add_endpoint(public='https://image.south.host/v1/', internal='https://image-internal.south.host/v1/', region='South') def test_building_a_service_catalog(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog self.assertEqual(sc.url_for(service_type='compute'), "https://compute.north.host/v1/1234") self.assertRaises(exceptions.EndpointNotFound, sc.url_for, region_name="South", service_type='compute') def test_service_catalog_endpoints(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog public_ep = sc.get_endpoints(service_type='compute', interface='publicURL') self.assertEqual(public_ep['compute'][1]['tenantId'], '2') self.assertEqual(public_ep['compute'][1]['versionId'], '1.1') self.assertEqual(public_ep['compute'][1]['internalURL'], "https://compute.north.host/v1.1/3456") def test_service_catalog_empty(self): self.AUTH_RESPONSE_BODY['access']['serviceCatalog'] = [] auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) self.assertRaises(exceptions.EmptyCatalog, auth_ref.service_catalog.url_for, service_type='image', interface='internalURL') def test_service_catalog_get_endpoints_region_names(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog endpoints = sc.get_endpoints(service_type='image', region_name='North') self.assertEqual(len(endpoints), 1) self.assertEqual(endpoints['image'][0]['publicURL'], 'https://image.north.host/v1/') endpoints = sc.get_endpoints(service_type='image', region_name='South') self.assertEqual(len(endpoints), 1) self.assertEqual(endpoints['image'][0]['publicURL'], 'https://image.south.host/v1/') endpoints = sc.get_endpoints(service_type='compute') self.assertEqual(len(endpoints['compute']), 2) endpoints = sc.get_endpoints(service_type='compute', region_name='North') self.assertEqual(len(endpoints['compute']), 2) endpoints = sc.get_endpoints(service_type='compute', region_name='West') self.assertEqual(len(endpoints['compute']), 0) def test_service_catalog_url_for_region_names(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog url = sc.url_for(service_type='image', region_name='North') self.assertEqual(url, 'https://image.north.host/v1/') url = sc.url_for(service_type='image', region_name='South') self.assertEqual(url, 'https://image.south.host/v1/') self.assertRaises(exceptions.EndpointNotFound, sc.url_for, service_type='image', region_name='West') def test_servcie_catalog_get_url_region_names(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog urls = sc.get_urls(service_type='image') self.assertEqual(len(urls), 2) urls = sc.get_urls(service_type='image', region_name='North') self.assertEqual(len(urls), 1) self.assertEqual(urls[0], 'https://image.north.host/v1/') urls = sc.get_urls(service_type='image', region_name='South') self.assertEqual(len(urls), 1) self.assertEqual(urls[0], 'https://image.south.host/v1/') urls = sc.get_urls(service_type='image', region_name='West') self.assertEqual(len(urls), 0) def test_service_catalog_service_name(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog url = sc.url_for(service_name='Image Servers', interface='public', service_type='image', region_name='North') self.assertEqual('https://image.north.host/v1/', url) self.assertRaises(exceptions.EndpointNotFound, sc.url_for, service_name='Image Servers', service_type='compute') urls = sc.get_urls(service_type='image', service_name='Image Servers', interface='public') self.assertIn('https://image.north.host/v1/', urls) self.assertIn('https://image.south.host/v1/', urls) urls = sc.get_urls(service_type='image', service_name='Servers', interface='public') self.assertEqual(0, len(urls)) def test_service_catalog_multiple_service_types(self): token = fixture.V2Token() token.set_scope() for i in range(3): s = token.add_service('compute') s.add_endpoint(public='public-%d' % i, admin='admin-%d' % i, internal='internal-%d' % i, region='region-%d' % i) auth_ref = access.create(body=token) urls = auth_ref.service_catalog.get_urls(service_type='compute', interface='publicURL') self.assertEqual(set(['public-0', 'public-1', 'public-2']), set(urls)) urls = auth_ref.service_catalog.get_urls(service_type='compute', interface='publicURL', region_name='region-1') self.assertEqual(('public-1', ), urls) def test_service_catalog_endpoint_id(self): token = fixture.V2Token() token.set_scope() endpoint_id = uuid.uuid4().hex public_url = uuid.uuid4().hex s = token.add_service('compute') s.add_endpoint(public=public_url, id=endpoint_id) s.add_endpoint(public=uuid.uuid4().hex) auth_ref = access.create(body=token) # initially assert that we get back all our urls for a simple filter urls = auth_ref.service_catalog.get_urls(interface='public') self.assertEqual(2, len(urls)) urls = auth_ref.service_catalog.get_urls(endpoint_id=endpoint_id, interface='public') self.assertEqual((public_url, ), urls) # with bad endpoint_id nothing should be found urls = auth_ref.service_catalog.get_urls(endpoint_id=uuid.uuid4().hex, interface='public') self.assertEqual(0, len(urls)) # we ignore a service_id because v2 doesn't know what it is urls = auth_ref.service_catalog.get_urls(endpoint_id=endpoint_id, service_id=uuid.uuid4().hex, interface='public') self.assertEqual((public_url, ), urls) def test_service_catalog_without_service_type(self): token = fixture.V2Token() token.set_scope() public_urls = [] for i in range(0, 3): public_url = uuid.uuid4().hex public_urls.append(public_url) s = token.add_service(uuid.uuid4().hex) s.add_endpoint(public=public_url) auth_ref = access.create(body=token) urls = auth_ref.service_catalog.get_urls(service_type=None, interface='public') self.assertEqual(3, len(urls)) for p in public_urls: self.assertIn(p, urls) keystoneauth-2.4.1/keystoneauth1/tests/unit/access/test_v3_access.py000066400000000000000000000165731271245473000257320ustar00rootroot00000000000000# 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 uuid import datetime from oslo_utils import timeutils from keystoneauth1 import access from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils class AccessV3Test(utils.TestCase): def test_building_unscoped_accessinfo(self): token = fixture.V3Token() token_id = uuid.uuid4().hex auth_ref = access.create(body=token, auth_token=token_id) self.assertIn('methods', auth_ref._data['token']) self.assertFalse(auth_ref.has_service_catalog()) self.assertNotIn('catalog', auth_ref._data['token']) self.assertEqual(token_id, auth_ref.auth_token) self.assertEqual(token.user_name, auth_ref.username) self.assertEqual(token.user_id, auth_ref.user_id) self.assertEqual(auth_ref.role_ids, []) self.assertEqual(auth_ref.role_names, []) self.assertIsNone(auth_ref.project_name) self.assertIsNone(auth_ref.project_id) self.assertFalse(auth_ref.domain_scoped) self.assertFalse(auth_ref.project_scoped) self.assertEqual(token.user_domain_id, auth_ref.user_domain_id) self.assertEqual(token.user_domain_name, auth_ref.user_domain_name) self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertEqual(auth_ref.expires, timeutils.parse_isotime( token['token']['expires_at'])) self.assertEqual(auth_ref.issued, timeutils.parse_isotime( token['token']['issued_at'])) self.assertEqual(auth_ref.expires, token.expires) self.assertEqual(auth_ref.issued, token.issued) self.assertEqual(auth_ref.audit_id, token.audit_id) self.assertIsNone(auth_ref.audit_chain_id) self.assertIsNone(token.audit_chain_id) self.assertIsNone(auth_ref.bind) def test_will_expire_soon(self): expires = timeutils.utcnow() + datetime.timedelta(minutes=5) token = fixture.V3Token(expires=expires) auth_ref = access.create(body=token) self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) self.assertTrue(auth_ref.will_expire_soon(stale_duration=301)) self.assertFalse(auth_ref.will_expire_soon()) def test_building_domain_scoped_accessinfo(self): token = fixture.V3Token() token.set_domain_scope() s = token.add_service(type='identity') s.add_standard_endpoints(public='http://url') token_id = uuid.uuid4().hex auth_ref = access.create(body=token, auth_token=token_id) self.assertTrue(auth_ref) self.assertIn('methods', auth_ref._data['token']) self.assertIn('catalog', auth_ref._data['token']) self.assertTrue(auth_ref.has_service_catalog()) self.assertTrue(auth_ref._data['token']['catalog']) self.assertEqual(token_id, auth_ref.auth_token) self.assertEqual(token.user_name, auth_ref.username) self.assertEqual(token.user_id, auth_ref.user_id) self.assertEqual(token.role_ids, auth_ref.role_ids) self.assertEqual(token.role_names, auth_ref.role_names) self.assertEqual(token.domain_name, auth_ref.domain_name) self.assertEqual(token.domain_id, auth_ref.domain_id) self.assertIsNone(auth_ref.project_name) self.assertIsNone(auth_ref.project_id) self.assertEqual(token.user_domain_id, auth_ref.user_domain_id) self.assertEqual(token.user_domain_name, auth_ref.user_domain_name) self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertTrue(auth_ref.domain_scoped) self.assertFalse(auth_ref.project_scoped) self.assertEqual(token.audit_id, auth_ref.audit_id) self.assertEqual(token.audit_chain_id, auth_ref.audit_chain_id) def test_building_project_scoped_accessinfo(self): token = fixture.V3Token() token.set_project_scope() s = token.add_service(type='identity') s.add_standard_endpoints(public='http://url') token_id = uuid.uuid4().hex auth_ref = access.create(body=token, auth_token=token_id) self.assertIn('methods', auth_ref._data['token']) self.assertIn('catalog', auth_ref._data['token']) self.assertTrue(auth_ref.has_service_catalog()) self.assertTrue(auth_ref._data['token']['catalog']) self.assertEqual(token_id, auth_ref.auth_token) self.assertEqual(token.user_name, auth_ref.username) self.assertEqual(token.user_id, auth_ref.user_id) self.assertEqual(token.role_ids, auth_ref.role_ids) self.assertEqual(token.role_names, auth_ref.role_names) self.assertIsNone(auth_ref.domain_name) self.assertIsNone(auth_ref.domain_id) self.assertEqual(token.project_name, auth_ref.project_name) self.assertEqual(token.project_id, auth_ref.project_id) self.assertEqual(auth_ref.tenant_name, auth_ref.project_name) self.assertEqual(auth_ref.tenant_id, auth_ref.project_id) self.assertEqual(token.project_domain_id, auth_ref.project_domain_id) self.assertEqual(token.project_domain_name, auth_ref.project_domain_name) self.assertEqual(token.user_domain_id, auth_ref.user_domain_id) self.assertEqual(token.user_domain_name, auth_ref.user_domain_name) self.assertFalse(auth_ref.domain_scoped) self.assertTrue(auth_ref.project_scoped) self.assertEqual(token.audit_id, auth_ref.audit_id) self.assertEqual(token.audit_chain_id, auth_ref.audit_chain_id) def test_oauth_access(self): consumer_id = uuid.uuid4().hex access_token_id = uuid.uuid4().hex token = fixture.V3Token() token.set_project_scope() token.set_oauth(access_token_id=access_token_id, consumer_id=consumer_id) auth_ref = access.create(body=token) self.assertEqual(consumer_id, auth_ref.oauth_consumer_id) self.assertEqual(access_token_id, auth_ref.oauth_access_token_id) self.assertEqual(consumer_id, auth_ref._data['token']['OS-OAUTH1']['consumer_id']) self.assertEqual( access_token_id, auth_ref._data['token']['OS-OAUTH1']['access_token_id']) def test_federated_property_standard_token(self): """Check if is_federated property returns expected value.""" token = fixture.V3Token() token.set_project_scope() auth_ref = access.create(body=token) self.assertFalse(auth_ref.is_federated) def test_binding(self): token = fixture.V3Token() principal = uuid.uuid4().hex token.set_bind('kerberos', principal) auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV3) self.assertEqual({'kerberos': principal}, auth_ref.bind) keystoneauth-2.4.1/keystoneauth1/tests/unit/access/test_v3_service_catalog.py000066400000000000000000000405361271245473000276170ustar00rootroot00000000000000# 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 uuid from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils class ServiceCatalogTest(utils.TestCase): def setUp(self): super(ServiceCatalogTest, self).setUp() self.AUTH_RESPONSE_BODY = fixture.V3Token( audit_chain_id=uuid.uuid4().hex) self.AUTH_RESPONSE_BODY.set_project_scope() self.AUTH_RESPONSE_BODY.add_role(name='admin') self.AUTH_RESPONSE_BODY.add_role(name='member') s = self.AUTH_RESPONSE_BODY.add_service('compute', name='nova') s.add_standard_endpoints( public='https://compute.north.host/novapi/public', internal='https://compute.north.host/novapi/internal', admin='https://compute.north.host/novapi/admin', region='North') s = self.AUTH_RESPONSE_BODY.add_service('object-store', name='swift') s.add_standard_endpoints( public='http://swift.north.host/swiftapi/public', internal='http://swift.north.host/swiftapi/internal', admin='http://swift.north.host/swiftapi/admin', region='South') s = self.AUTH_RESPONSE_BODY.add_service('image', name='glance') s.add_standard_endpoints( public='http://glance.north.host/glanceapi/public', internal='http://glance.north.host/glanceapi/internal', admin='http://glance.north.host/glanceapi/admin', region='North') s.add_standard_endpoints( public='http://glance.south.host/glanceapi/public', internal='http://glance.south.host/glanceapi/internal', admin='http://glance.south.host/glanceapi/admin', region='South') self.north_endpoints = {'public': 'http://glance.north.host/glanceapi/public', 'internal': 'http://glance.north.host/glanceapi/internal', 'admin': 'http://glance.north.host/glanceapi/admin'} self.south_endpoints = {'public': 'http://glance.south.host/glanceapi/public', 'internal': 'http://glance.south.host/glanceapi/internal', 'admin': 'http://glance.south.host/glanceapi/admin'} def test_building_a_service_catalog(self): auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog self.assertEqual(sc.url_for(service_type='compute'), "https://compute.north.host/novapi/public") self.assertEqual(sc.url_for(service_type='compute', interface='internal'), "https://compute.north.host/novapi/internal") self.assertRaises(exceptions.EndpointNotFound, sc.url_for, region_name='South', service_type='compute') def test_service_catalog_endpoints(self): auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog public_ep = sc.get_endpoints(service_type='compute', interface='public') self.assertEqual(public_ep['compute'][0]['region'], 'North') self.assertEqual(public_ep['compute'][0]['url'], "https://compute.north.host/novapi/public") def test_service_catalog_regions(self): self.AUTH_RESPONSE_BODY['token']['region_name'] = "North" auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog url = sc.url_for(service_type='image', interface='public') self.assertEqual(url, "http://glance.north.host/glanceapi/public") self.AUTH_RESPONSE_BODY['token']['region_name'] = "South" auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog url = sc.url_for(service_type='image', region_name="South", interface='internal') self.assertEqual(url, "http://glance.south.host/glanceapi/internal") def test_service_catalog_empty(self): self.AUTH_RESPONSE_BODY['token']['catalog'] = [] auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) self.assertRaises(exceptions.EmptyCatalog, auth_ref.service_catalog.url_for, service_type='image', interface='internalURL') def test_service_catalog_get_endpoints_region_names(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog endpoints = sc.get_endpoints(service_type='image', region_name='North') self.assertEqual(len(endpoints), 1) for endpoint in endpoints['image']: self.assertEqual(endpoint['url'], self.north_endpoints[endpoint['interface']]) endpoints = sc.get_endpoints(service_type='image', region_name='South') self.assertEqual(len(endpoints), 1) for endpoint in endpoints['image']: self.assertEqual(endpoint['url'], self.south_endpoints[endpoint['interface']]) endpoints = sc.get_endpoints(service_type='compute') self.assertEqual(len(endpoints['compute']), 3) endpoints = sc.get_endpoints(service_type='compute', region_name='North') self.assertEqual(len(endpoints['compute']), 3) endpoints = sc.get_endpoints(service_type='compute', region_name='West') self.assertEqual(len(endpoints['compute']), 0) def test_service_catalog_url_for_region_names(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog url = sc.url_for(service_type='image', region_name='North') self.assertEqual(url, self.north_endpoints['public']) url = sc.url_for(service_type='image', region_name='South') self.assertEqual(url, self.south_endpoints['public']) self.assertRaises(exceptions.EndpointNotFound, sc.url_for, service_type='image', region_name='West') def test_service_catalog_get_url_region_names(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog urls = sc.get_urls(service_type='image') self.assertEqual(len(urls), 2) urls = sc.get_urls(service_type='image', region_name='North') self.assertEqual(len(urls), 1) self.assertEqual(urls[0], self.north_endpoints['public']) urls = sc.get_urls(service_type='image', region_name='South') self.assertEqual(len(urls), 1) self.assertEqual(urls[0], self.south_endpoints['public']) urls = sc.get_urls(service_type='image', region_name='West') self.assertEqual(len(urls), 0) def test_service_catalog_service_name(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog url = sc.url_for(service_name='glance', interface='public', service_type='image', region_name='North') self.assertEqual('http://glance.north.host/glanceapi/public', url) url = sc.url_for(service_name='glance', interface='public', service_type='image', region_name='South') self.assertEqual('http://glance.south.host/glanceapi/public', url) self.assertRaises(exceptions.EndpointNotFound, sc.url_for, service_name='glance', service_type='compute') urls = sc.get_urls(service_type='image', service_name='glance', interface='public') self.assertIn('http://glance.north.host/glanceapi/public', urls) self.assertIn('http://glance.south.host/glanceapi/public', urls) urls = sc.get_urls(service_type='image', service_name='Servers', interface='public') self.assertEqual(0, len(urls)) def test_service_catalog_without_name(self): f = fixture.V3Token(audit_chain_id=uuid.uuid4().hex) if not f.project_id: f.set_project_scope() f.add_role(name='admin') f.add_role(name='member') region = 'RegionOne' tenant = '225da22d3ce34b15877ea70b2a575f58' s = f.add_service('volume') s.add_standard_endpoints( public='http://public.com:8776/v1/%s' % tenant, internal='http://internal:8776/v1/%s' % tenant, admin='http://admin:8776/v1/%s' % tenant, region=region) s = f.add_service('image') s.add_standard_endpoints(public='http://public.com:9292/v1', internal='http://internal:9292/v1', admin='http://admin:9292/v1', region=region) s = f.add_service('compute') s.add_standard_endpoints( public='http://public.com:8774/v2/%s' % tenant, internal='http://internal:8774/v2/%s' % tenant, admin='http://admin:8774/v2/%s' % tenant, region=region) s = f.add_service('ec2') s.add_standard_endpoints( public='http://public.com:8773/services/Cloud', internal='http://internal:8773/services/Cloud', admin='http://admin:8773/services/Admin', region=region) s = f.add_service('identity') s.add_standard_endpoints(public='http://public.com:5000/v3', internal='http://internal:5000/v3', admin='http://admin:35357/v3', region=region) pr_auth_ref = access.create(body=f) pr_sc = pr_auth_ref.service_catalog # this will work because there are no service names on that token url_ref = 'http://public.com:8774/v2/225da22d3ce34b15877ea70b2a575f58' url = pr_sc.url_for(service_type='compute', service_name='NotExist', interface='public') self.assertEqual(url_ref, url) ab_auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) ab_sc = ab_auth_ref.service_catalog # this won't work because there is a name and it's not this one self.assertRaises(exceptions.EndpointNotFound, ab_sc.url_for, service_type='compute', service_name='NotExist', interface='public') class ServiceCatalogV3Test(ServiceCatalogTest): def test_building_a_service_catalog(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog self.assertEqual(sc.url_for(service_type='compute'), 'https://compute.north.host/novapi/public') self.assertEqual(sc.url_for(service_type='compute', interface='internal'), 'https://compute.north.host/novapi/internal') self.assertRaises(exceptions.EndpointNotFound, sc.url_for, region_name='South', service_type='compute') def test_service_catalog_endpoints(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog public_ep = sc.get_endpoints(service_type='compute', interface='public') self.assertEqual(public_ep['compute'][0]['region_id'], 'North') self.assertEqual(public_ep['compute'][0]['url'], 'https://compute.north.host/novapi/public') def test_service_catalog_multiple_service_types(self): token = fixture.V3Token() token.set_project_scope() for i in range(3): s = token.add_service('compute') s.add_standard_endpoints(public='public-%d' % i, admin='admin-%d' % i, internal='internal-%d' % i, region='region-%d' % i) auth_ref = access.create(resp=None, body=token) urls = auth_ref.service_catalog.get_urls(service_type='compute', interface='public') self.assertEqual(set(['public-0', 'public-1', 'public-2']), set(urls)) urls = auth_ref.service_catalog.get_urls(service_type='compute', interface='public', region_name='region-1') self.assertEqual(('public-1', ), urls) def test_service_catalog_endpoint_id(self): token = fixture.V3Token() token.set_project_scope() service_id = uuid.uuid4().hex endpoint_id = uuid.uuid4().hex public_url = uuid.uuid4().hex s = token.add_service('compute', id=service_id) s.add_endpoint('public', public_url, id=endpoint_id) s.add_endpoint('public', uuid.uuid4().hex) auth_ref = access.create(body=token) # initially assert that we get back all our urls for a simple filter urls = auth_ref.service_catalog.get_urls(service_type='compute', interface='public') self.assertEqual(2, len(urls)) # with bad endpoint_id nothing should be found urls = auth_ref.service_catalog.get_urls(service_type='compute', endpoint_id=uuid.uuid4().hex, interface='public') self.assertEqual(0, len(urls)) # with service_id we get back both public endpoints urls = auth_ref.service_catalog.get_urls(service_type='compute', service_id=service_id, interface='public') self.assertEqual(2, len(urls)) # with service_id and endpoint_id we get back the url we want urls = auth_ref.service_catalog.get_urls(service_type='compute', service_id=service_id, endpoint_id=endpoint_id, interface='public') self.assertEqual((public_url, ), urls) # with service_id and endpoint_id we get back the url we want urls = auth_ref.service_catalog.get_urls(service_type='compute', endpoint_id=endpoint_id, interface='public') self.assertEqual((public_url, ), urls) def test_service_catalog_without_service_type(self): token = fixture.V3Token() token.set_project_scope() public_urls = [] for i in range(0, 3): public_url = uuid.uuid4().hex public_urls.append(public_url) s = token.add_service(uuid.uuid4().hex) s.add_endpoint('public', public_url) auth_ref = access.create(body=token) urls = auth_ref.service_catalog.get_urls(interface='public') self.assertEqual(3, len(urls)) for p in public_urls: self.assertIn(p, urls) keystoneauth-2.4.1/keystoneauth1/tests/unit/client_fixtures.py000066400000000000000000000114761271245473000247650ustar00rootroot00000000000000# 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 keystoneauth1 import fixture def project_scoped_token(): f = fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a', user_name='exampleuser', user_domain_id='4e6893b7ba0b4006840c3845660b86ed', user_domain_name='exampledomain', expires='2010-11-01T03:32:15-05:00', project_id='225da22d3ce34b15877ea70b2a575f58', project_name='exampleproject', project_domain_id='4e6893b7ba0b4006840c3845660b86ed', project_domain_name='exampledomain') f.add_role(id='76e72a', name='admin') f.add_role(id='f4f392', name='member') region = 'RegionOne' tenant = '225da22d3ce34b15877ea70b2a575f58' s = f.add_service('volume') s.add_standard_endpoints(public='http://public.com:8776/v1/%s' % tenant, internal='http://internal:8776/v1/%s' % tenant, admin='http://admin:8776/v1/%s' % tenant, region=region) s = f.add_service('image') s.add_standard_endpoints(public='http://public.com:9292/v1', internal='http://internal:9292/v1', admin='http://admin:9292/v1', region=region) s = f.add_service('compute') s.add_standard_endpoints(public='http://public.com:8774/v2/%s' % tenant, internal='http://internal:8774/v2/%s' % tenant, admin='http://admin:8774/v2/%s' % tenant, region=region) s = f.add_service('ec2') s.add_standard_endpoints(public='http://public.com:8773/services/Cloud', internal='http://internal:8773/services/Cloud', admin='http://admin:8773/services/Admin', region=region) s = f.add_service('identity') s.add_standard_endpoints(public='http://public.com:5000/v3', internal='http://internal:5000/v3', admin='http://admin:35357/v3', region=region) return f def domain_scoped_token(): f = fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a', user_name='exampleuser', user_domain_id='4e6893b7ba0b4006840c3845660b86ed', user_domain_name='exampledomain', expires='2010-11-01T03:32:15-05:00', domain_id='8e9283b7ba0b1038840c3842058b86ab', domain_name='anotherdomain') f.add_role(id='76e72a', name='admin') f.add_role(id='f4f392', name='member') region = 'RegionOne' s = f.add_service('volume') s.add_standard_endpoints(public='http://public.com:8776/v1/None', internal='http://internal.com:8776/v1/None', admin='http://admin.com:8776/v1/None', region=region) s = f.add_service('image') s.add_standard_endpoints(public='http://public.com:9292/v1', internal='http://internal:9292/v1', admin='http://admin:9292/v1', region=region) s = f.add_service('compute') s.add_standard_endpoints(public='http://public.com:8774/v1.1/None', internal='http://internal:8774/v1.1/None', admin='http://admin:8774/v1.1/None', region=region) s = f.add_service('ec2') s.add_standard_endpoints(public='http://public.com:8773/services/Cloud', internal='http://internal:8773/services/Cloud', admin='http://admin:8773/services/Admin', region=region) s = f.add_service('identity') s.add_standard_endpoints(public='http://public.com:5000/v3', internal='http://internal:5000/v3', admin='http://admin:35357/v3', region=region) return f AUTH_SUBJECT_TOKEN = '3e2813b7ba0b4006840c3825860b86ed' AUTH_RESPONSE_HEADERS = { 'X-Subject-Token': AUTH_SUBJECT_TOKEN, } keystoneauth-2.4.1/keystoneauth1/tests/unit/data/000077500000000000000000000000001271245473000221045ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/data/README000066400000000000000000000005211271245473000227620ustar00rootroot00000000000000This directory holds the betamax test cassettes that are pre-generated for unit testing. This can be removed in the future with a functional test that stands up a full devstack, records a cassette and then replays it as part of the test suite. Until the functional testing is implemented do not remove this directory or enclosed files. keystoneauth-2.4.1/keystoneauth1/tests/unit/data/ksa_betamax_test_cassette.json000066400000000000000000000031761271245473000302170ustar00rootroot00000000000000{"http_interactions": [{"request": {"body": {"string": "{\"auth\": {\"tenantName\": \"test_tenant_name\", \"passwordCredentials\": {\"username\": \"test_user_name\", \"password\": \"test_password\"}}}", "encoding": "utf-8"}, "headers": {"Content-Length": ["128"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["application/json"], "User-Agent": ["keystoneauth1"], "Connection": ["keep-alive"], "Content-Type": ["application/json"]}, "method": "POST", "uri": "http://keystonauth.betamax_test/v2.0/tokens"}, "response": {"body": {"string": "{\"access\": {\"token\": {\"issued_at\": \"2015-11-27T15:17:19.755470\", \"expires\": \"2015-11-27T16:17:19Z\", \"id\": \"c000c5ee4ba04594a00886028584b50d\", \"tenant\": {\"description\": null, \"enabled\": true, \"id\": \"6932cad596634a61ac9c759fb91beef1\", \"name\": \"test_tenant_name\"}, \"audit_ids\": [\"jY3gYg_YTbmzY2a4ioGuCw\"]}, \"user\": {\"username\": \"test_user_name\", \"roles_links\": [], \"id\": \"96995e6cc15b40fa8e7cd762f6a5d4c0\", \"roles\": [{\"name\": \"_member_\"}], \"name\": \"67eff5f6-9477-4961-88b4-437e6596a795\"}, \"metadata\": {\"is_admin\": 0, \"roles\": [\"9fe2ff9ee4384b1894a90878d3e92bab\"]}}}", "encoding": null}, "headers": {"X-Openstack-Request-Id": ["req-f9e188b4-06fd-4a4c-a952-2315b368218c"], "Content-Length": ["2684"], "Connection": ["keep-alive"], "Date": ["Fri, 27 Nov 2015 15:17:19 GMT"], "Content-Type": ["application/json"], "Vary": ["X-Auth-Token"], "X-Distribution": ["Ubuntu"], "Server": ["Fake"]}, "status": {"message": "OK", "code": 200}, "url": "http://keystonauth.betamax_test/v2.0/tokens"}, "recorded_at": "2015-11-27T15:17:19"}], "recorded_with": "betamax/0.5.1"} keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/000077500000000000000000000000001271245473000225015ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/__init__.py000066400000000000000000000000001271245473000246000ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/kerberos/000077500000000000000000000000001271245473000243155ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/kerberos/__init__.py000066400000000000000000000000001271245473000264140ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/kerberos/base.py000066400000000000000000000031011271245473000255740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2011 OpenStack Foundation # 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. from keystoneauth1.tests.unit.extras.kerberos import utils from keystoneauth1.tests.unit import utils as test_utils REQUEST = {'auth': {'identity': {'methods': ['kerberos'], 'kerberos': {}}}} class TestCase(test_utils.TestCase): """Test case base class for Kerberos unit tests.""" TEST_V3_URL = test_utils.TestCase.TEST_ROOT_URL + 'v3' def setUp(self): super(TestCase, self).setUp() km = utils.KerberosMock(self.requests_mock) self.kerberos_mock = self.useFixture(km) def assertRequestBody(self, body=None): """Ensure the request body is the standard Kerberos auth request. :param dict body: the body to compare. If not provided the last request body will be used. """ if not body: body = self.requests_mock.last_request.json() self.assertEqual(REQUEST, body) keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/kerberos/test_mapped.py000066400000000000000000000055251271245473000272030ustar00rootroot00000000000000# 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 uuid import six from keystoneauth1 import fixture as ks_fixture from keystoneauth1 import session from keystoneauth1.tests.unit.extras.kerberos import base try: # Until requests_kerberos gets py3 support, this is going to fail to import from keystoneauth1.extras import kerberos except ImportError: if six.PY2: # requests_kerberos is expected to be there on py2, so don't ignore. raise # requests_kerberos isn't available kerberos = False class TestMappedAuth(base.TestCase): def setUp(self): if not kerberos: self.skipTest("Kerberos support isn't available.") super(TestMappedAuth, self).setUp() self.protocol = uuid.uuid4().hex self.identity_provider = uuid.uuid4().hex @property def token_url(self): fmt = '%s/OS-FEDERATION/identity_providers/%s/protocols/%s/auth' return fmt % ( self.TEST_V3_URL, self.identity_provider, self.protocol) def test_unscoped_mapped_auth(self): token_id, _ = self.kerberos_mock.mock_auth_success( url=self.token_url, method='GET') plugin = kerberos.MappedKerberos( auth_url=self.TEST_V3_URL, protocol=self.protocol, identity_provider=self.identity_provider) sess = session.Session() tok = plugin.get_token(sess) self.assertEqual(token_id, tok) def test_project_scoped_mapped_auth(self): self.kerberos_mock.mock_auth_success(url=self.token_url, method='GET') scoped_id = uuid.uuid4().hex scoped_body = ks_fixture.V3Token() scoped_body.set_project_scope() self.requests_mock.post( '%s/auth/tokens' % self.TEST_V3_URL, json=scoped_body, headers={'X-Subject-Token': scoped_id, 'Content-Type': 'application/json'}) plugin = kerberos.MappedKerberos( auth_url=self.TEST_V3_URL, protocol=self.protocol, identity_provider=self.identity_provider, project_id=scoped_body.project_id) sess = session.Session() tok = plugin.get_token(sess) proj = plugin.get_project_id(sess) self.assertEqual(scoped_id, tok) self.assertEqual(scoped_body.project_id, proj) keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/kerberos/test_v3.py000066400000000000000000000032451271245473000262620ustar00rootroot00000000000000# 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 from keystoneauth1 import session from keystoneauth1.tests.unit.extras.kerberos import base try: # Until requests_kerberos gets py3 support, this is going to fail to import from keystoneauth1.extras import kerberos except ImportError: if six.PY2: # requests_kerberos is expected to be there on py2, so don't ignore. raise # requests_kerberos isn't available kerberos = None class TestKerberosAuth(base.TestCase): def setUp(self): if not kerberos: self.skipTest("Kerberos support isn't available.") super(TestKerberosAuth, self).setUp() def test_authenticate_with_kerberos_domain_scoped(self): token_id, token_body = self.kerberos_mock.mock_auth_success() a = kerberos.Kerberos(self.TEST_ROOT_URL + 'v3') s = session.Session(a) token = a.get_token(s) self.assertRequestBody() self.assertEqual( self.kerberos_mock.challenge_header, self.requests_mock.last_request.headers['Authorization']) self.assertEqual(token_id, a.auth_ref.auth_token) self.assertEqual(token_id, token) keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/kerberos/utils.py000066400000000000000000000056111271245473000260320ustar00rootroot00000000000000# 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 uuid import fixtures from oslotest import mockpatch try: # requests_kerberos won't be available on py3, it doesn't work with py3. import requests_kerberos except ImportError: requests_kerberos = None from keystoneauth1 import fixture as ks_fixture from keystoneauth1.tests.unit import utils as test_utils class KerberosMock(fixtures.Fixture): def __init__(self, requests_mock): super(KerberosMock, self).__init__() self.challenge_header = 'Negotiate %s' % uuid.uuid4().hex self.pass_header = 'Negotiate %s' % uuid.uuid4().hex self.requests_mock = requests_mock def setUp(self): super(KerberosMock, self).setUp() m = mockpatch.PatchObject(requests_kerberos.HTTPKerberosAuth, 'generate_request_header', self._generate_request_header) self.header_fixture = self.useFixture(m) m = mockpatch.PatchObject(requests_kerberos.HTTPKerberosAuth, 'authenticate_server', self._authenticate_server) self.authenticate_fixture = self.useFixture(m) def _generate_request_header(self, *args, **kwargs): return self.challenge_header def _authenticate_server(self, response): return response.headers.get('www-authenticate') == self.pass_header def mock_auth_success( self, token_id=None, token_body=None, method='POST', url=test_utils.TestCase.TEST_ROOT_URL + 'v3/auth/tokens'): if not token_id: token_id = uuid.uuid4().hex if not token_body: token_body = ks_fixture.V3Token() response_list = [{'text': 'Fail', 'status_code': 401, 'headers': {'WWW-Authenticate': 'Negotiate'}}, {'headers': {'X-Subject-Token': token_id, 'Content-Type': 'application/json', 'WWW-Authenticate': self.pass_header}, 'status_code': 200, 'json': token_body}] self.requests_mock.register_uri(method, url, response_list=response_list) return token_id, token_body keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/000077500000000000000000000000001271245473000235175ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/__init__.py000066400000000000000000000000001271245473000256160ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/examples/000077500000000000000000000000001271245473000253355ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/examples/xml/000077500000000000000000000000001271245473000261355ustar00rootroot00000000000000ADFS_RequestSecurityTokenResponse.xml000066400000000000000000000334721271245473000353060ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/examples/xml http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal urn:uuid:487c064b-b7c6-4654-b4d4-715f9961170e 2014-08-05T18:36:14.235Z 2014-08-05T18:41:14.235Z 2014-08-05T18:36:14.063Z 2014-08-05T19:36:14.063Z https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS marek.denis@cern.ch urn:oasis:names:tc:SAML:1.0:cm:bearer marek.denis@cern.ch marek.denis@cern.ch madenis CERN Users Domain Users occupants-bldg-31 CERN-Direct-Employees ca-dev-allowed cernts-cerntstest-users staf-fell-pjas-at-cern ELG-CERN student-club-new-members pawel-dynamic-test-82 Marek Kamil Denis +5555555 31S-013 Marek Kamil Denis CERN Registered CERN Normal marek.denis@cern.ch urn:oasis:names:tc:SAML:1.0:cm:bearer EaZ/2d0KAY5un9akV3++Npyk6hBc8JuTYs2S3lSxUeQ= CxYiYvNsbedhHdmDbb9YQCBy6Ppus3bNJdw2g2HLq0VU2yRhv23mUW05I89Hs4yG4OcCo0uOZ3zaeNFbSNXMW+Mr996tAXtujKjgyrCXNJAToE+gwltvGxwY1EluSbe3IzoSM3Ao87mKhxGOSzlDhuN7dQ9Rv6l/J4gUjbOO5SIX4pdZ6mVF7cHEfe9x+H8Lg15YjnElQUEaPi+NSW5jYTdtIpsB4ORxJvALuSt6+4doDYc9wuwBiWkEdnBHAQBINoKpAV2oy0/C85SBX3IdRhxUznmL5yEUmf8JvPccXecMPqJow0L43mnCdu74xPwU0as3MNfYQ10kLvHXHfIExg== MIIIEjCCBfqgAwIBAgIKLYgjvQAAAAAAMDANBgkqhkiG9w0BAQsFADBRMRIwEAYKCZImiZPyLGQBGRYCY2gxFDASBgoJkiaJk/IsZAEZFgRjZXJuMSUwIwYDVQQDExxDRVJOIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMTEwODA4Mzg1NVoXDTIzMDcyOTA5MTkzOFowVjESMBAGCgmSJomT8ixkARkWAmNoMRQwEgYKCZImiZPyLGQBGRYEY2VybjESMBAGA1UECxMJY29tcHV0ZXJzMRYwFAYDVQQDEw1sb2dpbi5jZXJuLmNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp6t1C0SGlLddL2M+ltffGioTnDT3eztOxlA9bAGuvB8/Rjym8en6+ET9boM02CyoR5Vpn8iElXVWccAExPIQEq70D6LPe86vb+tYhuKPeLfuICN9Z0SMQ4f+57vk61Co1/uw/8kPvXlyd+Ai8Dsn/G0hpH67bBI9VOQKfpJqclcSJuSlUB5PJffvMUpr29B0eRx8LKFnIHbDILSu6nVbFLcadtWIjbYvoKorXg3J6urtkz+zEDeYMTvA6ZGOFf/Xy5eGtroSq9csSC976tx+umKEPhXBA9AcpiCV9Cj5axN03Aaa+iTE36jpnjcd9d02dy5Q9jE2nUN6KXnB6qF6eQIDAQABo4ID5TCCA+EwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIg73QCYLtjQ2G7Ysrgd71N4WA0GIehd2yb4Wu9TkCAWQCARkwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIFoDBoBgNVHSAEYTBfMF0GCisGAQQBYAoEAQEwTzBNBggrBgEFBQcCARZBaHR0cDovL2NhLWRvY3MuY2Vybi5jaC9jYS1kb2NzL2NwLWNwcy9jZXJuLXRydXN0ZWQtY2EyLWNwLWNwcy5wZGYwJwYJKwYBBAGCNxUKBBowGDAKBggrBgEFBQcDAjAKBggrBgEFBQcDATAdBgNVHQ4EFgQUqtJcwUXasyM6sRaO5nCMFoFDenMwGAYDVR0RBBEwD4INbG9naW4uY2Vybi5jaDAfBgNVHSMEGDAWgBQdkBnqyM7MPI0UsUzZ7BTiYUADYTCCASoGA1UdHwSCASEwggEdMIIBGaCCARWgggERhkdodHRwOi8vY2FmaWxlcy5jZXJuLmNoL2NhZmlsZXMvY3JsL0NFUk4lMjBDZXJ0aWZpY2F0aW9uJTIwQXV0aG9yaXR5LmNybIaBxWxkYXA6Ly8vQ049Q0VSTiUyMENlcnRpZmljYXRpb24lMjBBdXRob3JpdHksQ049Q0VSTlBLSTA3LENOPUNEUCxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWNlcm4sREM9Y2g/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIIBVAYIKwYBBQUHAQEEggFGMIIBQjBcBggrBgEFBQcwAoZQaHR0cDovL2NhZmlsZXMuY2Vybi5jaC9jYWZpbGVzL2NlcnRpZmljYXRlcy9DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eS5jcnQwgbsGCCsGAQUFBzAChoGubGRhcDovLy9DTj1DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1jZXJuLERDPWNoP2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jZXJuLmNoL29jc3AwDQYJKoZIhvcNAQELBQADggIBAGKZ3bknTCfNuh4TMaL3PuvBFjU8LQ5NKY9GLZvY2ibYMRk5Is6eWRgyUsy1UJRQdaQQPnnysqrGq8VRw/NIFotBBsA978/+jj7v4e5Kr4o8HvwAQNLBxNmF6XkDytpLL701FcNEGRqIsoIhNzihi2VBADLC9HxljEyPT52IR767TMk/+xTOqClceq3sq6WRD4m+xaWRUJyOhn+Pqr+wbhXIw4wzHC6X0hcLj8P9Povtm6VmKkN9JPuymMo/0+zSrUt2+TYfmbbEKYJSP0+sceQ76IKxxmSdKAr1qDNE8v+c3DvPM2PKmfivwaV2l44FdP8ulzqTgphkYcN1daa9Oc+qJeyu/eL7xWzk6Zq5R+jVrMlM0p1y2XczI7Hoc96TMOcbVnwgMcVqRM9p57VItn6XubYPR0C33i1yUZjkWbIfqEjq6Vev6lVgngOyzu+hqC/8SDyORA3dlF9aZOD13kPZdF/JRphHREQtaRydAiYRlE/WHTvOcY52jujDftUR6oY0eWaWkwSHbX+kDFx8IlR8UtQCUgkGHBGwnOYLIGu7SRDGSfOBOiVhxKoHWVk/pL6eKY2SkmyOmmgO4JnQGg95qeAOMG/EQZt/2x8GAavUqGvYy9dPFwFf08678hQqkjNSuex7UD0ku8OP1QKvpP44l6vZhFc6A5XqjdU9lus1 _c9e77bc4-a81b-4da7-88c2-72a6ba376d3f _c9e77bc4-a81b-4da7-88c2-72a6ba376d3f urn:oasis:names:tc:SAML:1.0:assertion http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/examples/xml/ADFS_fault.xml000066400000000000000000000015331271245473000305710ustar00rootroot00000000000000 http://www.w3.org/2005/08/addressing/soap/fault urn:uuid:89c47849-2622-4cdc-bb06-1d46c89ed12d s:Sender a:FailedAuthentication At least one security token in the message could not be validated. keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/fixtures/000077500000000000000000000000001271245473000253705ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/fixtures/__init__.py000066400000000000000000000063601271245473000275060ustar00rootroot00000000000000# 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 string DIR = os.path.dirname(os.path.abspath(__file__)) def template(f, **kwargs): with open(os.path.join(DIR, 'templates', f)) as f: return string.Template(f.read()).substitute(**kwargs) def soap_response(**kwargs): kwargs.setdefault('provider', 'https://idp.testshib.org/idp/shibboleth') kwargs.setdefault('consumer', 'https://openstack4.local/Shibboleth.sso/SAML2/ECP') kwargs.setdefault('issuer', 'https://openstack4.local/shibboleth') return template('soap_response.xml', **kwargs).encode('utf-8') def saml_assertion(**kwargs): kwargs.setdefault('issuer', 'https://idp.testshib.org/idp/shibboleth') kwargs.setdefault('destination', 'https://openstack4.local/Shibboleth.sso/SAML2/ECP') return template('saml_assertion.xml', **kwargs).encode('utf-8') SP_SOAP_RESPONSE = soap_response() SAML2_ASSERTION = saml_assertion() UNSCOPED_TOKEN_HEADER = 'UNSCOPED_TOKEN' UNSCOPED_TOKEN = { "token": { "issued_at": "2014-06-09T09:48:59.643406Z", "extras": {}, "methods": ["saml2"], "expires_at": "2014-06-09T10:48:59.643375Z", "user": { "OS-FEDERATION": { "identity_provider": { "id": "testshib" }, "protocol": { "id": "saml2" }, "groups": [ {"id": "1764fa5cf69a49a4918131de5ce4af9a"} ] }, "id": "testhib%20user", "name": "testhib user" } } } PROJECTS = { "projects": [ { "domain_id": "37ef61", "enabled": 'true', "id": "12d706", "links": { "self": "http://identity:35357/v3/projects/12d706" }, "name": "a project name" }, { "domain_id": "37ef61", "enabled": 'true', "id": "9ca0eb", "links": { "self": "http://identity:35357/v3/projects/9ca0eb" }, "name": "another project" } ], "links": { "self": "http://identity:35357/v3/OS-FEDERATION/projects", "previous": 'null', "next": 'null' } } DOMAINS = { "domains": [ { "description": "desc of domain", "enabled": 'true', "id": "37ef61", "links": { "self": "http://identity:35357/v3/domains/37ef61" }, "name": "my domain" } ], "links": { "self": "http://identity:35357/v3/OS-FEDERATION/domains", "previous": 'null', "next": 'null' } } keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/fixtures/templates/000077500000000000000000000000001271245473000273665ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/fixtures/templates/saml_assertion.xml000066400000000000000000000071621271245473000331410ustar00rootroot00000000000000 x= $issuer VALUE== VALUE= keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/fixtures/templates/soap_response.xml000066400000000000000000000037111271245473000327720ustar00rootroot00000000000000 $issuer ss:mem:6f1f20fafbb38433467e9d477df67615 $issuer keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/test_auth_adfs.py000066400000000000000000000237051271245473000270750ustar00rootroot00000000000000# 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 uuid from lxml import etree from six.moves import urllib from keystoneauth1 import exceptions from keystoneauth1.extras import _saml2 as saml2 from keystoneauth1.tests.unit import client_fixtures from keystoneauth1.tests.unit.extras.saml2 import fixtures as saml2_fixtures from keystoneauth1.tests.unit.extras.saml2 import utils from keystoneauth1.tests.unit import matchers class AuthenticateviaADFSTests(utils.TestCase): GROUP = 'auth' NAMESPACES = { 's': 'http://www.w3.org/2003/05/soap-envelope', 'trust': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512', 'wsa': 'http://www.w3.org/2005/08/addressing', 'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy', 'a': 'http://www.w3.org/2005/08/addressing', 'o': ('http://docs.oasis-open.org/wss/2004/01/oasis' '-200401-wss-wssecurity-secext-1.0.xsd') } USER_XPATH = ('/s:Envelope/s:Header' '/o:Security' '/o:UsernameToken' '/o:Username') PASSWORD_XPATH = ('/s:Envelope/s:Header' '/o:Security' '/o:UsernameToken' '/o:Password') ADDRESS_XPATH = ('/s:Envelope/s:Body' '/trust:RequestSecurityToken' '/wsp:AppliesTo/wsa:EndpointReference' '/wsa:Address') TO_XPATH = ('/s:Envelope/s:Header' '/a:To') TEST_TOKEN = uuid.uuid4().hex PROTOCOL = 'saml2' @property def _uuid4(self): return '4b911420-4982-4009-8afc-5c596cd487f5' def setUp(self): super(AuthenticateviaADFSTests, self).setUp() self.IDENTITY_PROVIDER = 'adfs' self.IDENTITY_PROVIDER_URL = ('http://adfs.local/adfs/service/trust/13' '/usernamemixed') self.FEDERATION_AUTH_URL = '%s/%s' % ( self.TEST_URL, 'OS-FEDERATION/identity_providers/adfs/protocols/saml2/auth') self.SP_ENDPOINT = 'https://openstack4.local/Shibboleth.sso/ADFS' self.adfsplugin = saml2.V3ADFSPassword( self.TEST_URL, self.IDENTITY_PROVIDER, self.IDENTITY_PROVIDER_URL, self.SP_ENDPOINT, self.TEST_USER, self.TEST_TOKEN, self.PROTOCOL) self.ADFS_SECURITY_TOKEN_RESPONSE = utils._load_xml( 'ADFS_RequestSecurityTokenResponse.xml') self.ADFS_FAULT = utils._load_xml('ADFS_fault.xml') def test_get_adfs_security_token(self): """Test ADFSPassword._get_adfs_security_token().""" self.requests_mock.post( self.IDENTITY_PROVIDER_URL, content=utils.make_oneline(self.ADFS_SECURITY_TOKEN_RESPONSE), status_code=200) self.adfsplugin._prepare_adfs_request() self.adfsplugin._get_adfs_security_token(self.session) adfs_response = etree.tostring(self.adfsplugin.adfs_token) fixture_response = self.ADFS_SECURITY_TOKEN_RESPONSE self.assertThat(fixture_response, matchers.XMLEquals(adfs_response)) def test_adfs_request_user(self): self.adfsplugin._prepare_adfs_request() user = self.adfsplugin.prepared_request.xpath( self.USER_XPATH, namespaces=self.NAMESPACES)[0] self.assertEqual(self.TEST_USER, user.text) def test_adfs_request_password(self): self.adfsplugin._prepare_adfs_request() password = self.adfsplugin.prepared_request.xpath( self.PASSWORD_XPATH, namespaces=self.NAMESPACES)[0] self.assertEqual(self.TEST_TOKEN, password.text) def test_adfs_request_to(self): self.adfsplugin._prepare_adfs_request() to = self.adfsplugin.prepared_request.xpath( self.TO_XPATH, namespaces=self.NAMESPACES)[0] self.assertEqual(self.IDENTITY_PROVIDER_URL, to.text) def test_prepare_adfs_request_address(self): self.adfsplugin._prepare_adfs_request() address = self.adfsplugin.prepared_request.xpath( self.ADDRESS_XPATH, namespaces=self.NAMESPACES)[0] self.assertEqual(self.SP_ENDPOINT, address.text) def test_prepare_sp_request(self): assertion = etree.XML(self.ADFS_SECURITY_TOKEN_RESPONSE) assertion = assertion.xpath( saml2.V3ADFSPassword.ADFS_ASSERTION_XPATH, namespaces=saml2.V3ADFSPassword.ADFS_TOKEN_NAMESPACES) assertion = assertion[0] assertion = etree.tostring(assertion) assertion = assertion.replace( b'http://docs.oasis-open.org/ws-sx/ws-trust/200512', b'http://schemas.xmlsoap.org/ws/2005/02/trust') assertion = urllib.parse.quote(assertion) assertion = 'wa=wsignin1.0&wresult=' + assertion self.adfsplugin.adfs_token = etree.XML( self.ADFS_SECURITY_TOKEN_RESPONSE) self.adfsplugin._prepare_sp_request() self.assertEqual(assertion, self.adfsplugin.encoded_assertion) def test_get_adfs_security_token_authn_fail(self): """Test proper parsing XML fault after bad authentication. An exceptions.AuthorizationFailure should be raised including error message from the XML message indicating where was the problem. """ content = utils.make_oneline(self.ADFS_FAULT) self.requests_mock.register_uri('POST', self.IDENTITY_PROVIDER_URL, content=content, status_code=500) self.adfsplugin._prepare_adfs_request() self.assertRaises(exceptions.AuthorizationFailure, self.adfsplugin._get_adfs_security_token, self.session) # TODO(marek-denis): Python3 tests complain about missing 'message' # attributes # self.assertEqual('a:FailedAuthentication', e.message) def test_get_adfs_security_token_bad_response(self): """Test proper handling HTTP 500 and mangled (non XML) response. This should never happen yet, keystoneauth1 should be prepared and correctly raise exceptions.InternalServerError once it cannot parse XML fault message """ self.requests_mock.register_uri('POST', self.IDENTITY_PROVIDER_URL, content=b'NOT XML', status_code=500) self.adfsplugin._prepare_adfs_request() self.assertRaises(exceptions.InternalServerError, self.adfsplugin._get_adfs_security_token, self.session) # TODO(marek-denis): Need to figure out how to properly send cookies # from the request_mock methods. def _send_assertion_to_service_provider(self): """Test whether SP issues a cookie.""" cookie = uuid.uuid4().hex self.requests_mock.post(self.SP_ENDPOINT, headers={"set-cookie": cookie}, status_code=302) self.adfsplugin.adfs_token = self._build_adfs_request() self.adfsplugin._prepare_sp_request() self.adfsplugin._send_assertion_to_service_provider(self.session) self.assertEqual(1, len(self.session.session.cookies)) def test_send_assertion_to_service_provider_bad_status(self): self.requests_mock.register_uri('POST', self.SP_ENDPOINT, status_code=500) self.adfsplugin.adfs_token = etree.XML( self.ADFS_SECURITY_TOKEN_RESPONSE) self.adfsplugin._prepare_sp_request() self.assertRaises( exceptions.InternalServerError, self.adfsplugin._send_assertion_to_service_provider, self.session) def test_access_sp_no_cookies_fail(self): # clean cookie jar self.session.session.cookies = [] self.assertRaises(exceptions.AuthorizationFailure, self.adfsplugin._access_service_provider, self.session) def test_check_valid_token_when_authenticated(self): self.requests_mock.register_uri( 'GET', self.FEDERATION_AUTH_URL, json=saml2_fixtures.UNSCOPED_TOKEN, headers=client_fixtures.AUTH_RESPONSE_HEADERS) self.session.session.cookies = [object()] self.adfsplugin._access_service_provider(self.session) response = self.adfsplugin.authenticated_response self.assertEqual(client_fixtures.AUTH_RESPONSE_HEADERS, response.headers) self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], response.json()['token']) def test_end_to_end_workflow(self): self.requests_mock.register_uri( 'POST', self.IDENTITY_PROVIDER_URL, content=self.ADFS_SECURITY_TOKEN_RESPONSE, status_code=200) self.requests_mock.register_uri( 'POST', self.SP_ENDPOINT, headers={"set-cookie": 'x'}, status_code=302) self.requests_mock.register_uri( 'GET', self.FEDERATION_AUTH_URL, json=saml2_fixtures.UNSCOPED_TOKEN, headers=client_fixtures.AUTH_RESPONSE_HEADERS) # NOTE(marek-denis): We need to mimic this until self.requests_mock can # issue cookies properly. self.session.session.cookies = [object()] token = self.adfsplugin.get_auth_ref(self.session) self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token) keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/test_auth_saml2.py000066400000000000000000000247201271245473000271740ustar00rootroot00000000000000# 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 uuid from lxml import etree from keystoneauth1 import exceptions from keystoneauth1.extras import _saml2 as saml2 from keystoneauth1.tests.unit.extras.saml2 import fixtures as saml2_fixtures from keystoneauth1.tests.unit.extras.saml2 import utils from keystoneauth1.tests.unit import matchers class AuthenticateviaSAML2Tests(utils.TestCase): GROUP = 'auth' TEST_TOKEN = uuid.uuid4().hex def setUp(self): super(AuthenticateviaSAML2Tests, self).setUp() self.ECP_SP_EMPTY_REQUEST_HEADERS = { 'Accept': 'text/html; application/vnd.paos+xml', 'PAOS': ('ver="urn:liberty:paos:2003-08";' '"urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"') } self.ECP_SP_SAML2_REQUEST_HEADERS = { 'Content-Type': 'application/vnd.paos+xml' } self.ECP_SAML2_NAMESPACES = { 'ecp': 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp', 'S': 'http://schemas.xmlsoap.org/soap/envelope/', 'paos': 'urn:liberty:paos:2003-08' } self.ECP_RELAY_STATE = '//ecp:RelayState' self.ECP_SERVICE_PROVIDER_CONSUMER_URL = ('/S:Envelope/S:Header/paos:' 'Request/' '@responseConsumerURL') self.ECP_IDP_CONSUMER_URL = ('/S:Envelope/S:Header/ecp:Response/' '@AssertionConsumerServiceURL') self.IDENTITY_PROVIDER = 'testidp' self.IDENTITY_PROVIDER_URL = 'http://local.url' self.PROTOCOL = 'saml2' self.FEDERATION_AUTH_URL = '%s/%s' % ( self.TEST_URL, 'OS-FEDERATION/identity_providers/testidp/protocols/saml2/auth') self.SHIB_CONSUMER_URL = ('https://openstack4.local/' 'Shibboleth.sso/SAML2/ECP') self.saml2plugin = saml2.V3Saml2Password( self.TEST_URL, self.IDENTITY_PROVIDER, self.IDENTITY_PROVIDER_URL, self.TEST_USER, self.TEST_TOKEN, self.PROTOCOL) def test_initial_sp_call(self): """Test initial call, expect SOAP message.""" self.requests_mock.get( self.FEDERATION_AUTH_URL, content=utils.make_oneline(saml2_fixtures.SP_SOAP_RESPONSE)) a = self.saml2plugin._send_service_provider_request(self.session) self.assertFalse(a) sp_soap_response = etree.tostring(self.saml2plugin.saml2_authn_request) self.assertThat(saml2_fixtures.SP_SOAP_RESPONSE, matchers.XMLEquals(sp_soap_response)) self.assertEqual( self.saml2plugin.sp_response_consumer_url, self.SHIB_CONSUMER_URL, "Expected consumer_url set to %s instead of %s" % ( self.SHIB_CONSUMER_URL, str(self.saml2plugin.sp_response_consumer_url))) def test_initial_sp_call_when_saml_authenticated(self): self.requests_mock.get( self.FEDERATION_AUTH_URL, json=saml2_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) a = self.saml2plugin._send_service_provider_request(self.session) self.assertTrue(a) self.assertEqual( saml2_fixtures.UNSCOPED_TOKEN['token'], self.saml2plugin.authenticated_response.json()['token']) self.assertEqual( saml2_fixtures.UNSCOPED_TOKEN_HEADER, self.saml2plugin.authenticated_response.headers['X-Subject-Token']) def test_get_unscoped_token_when_authenticated(self): self.requests_mock.get( self.FEDERATION_AUTH_URL, json=saml2_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER, 'Content-Type': 'application/json'}) token = self.saml2plugin.get_auth_ref(self.session) self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, token.auth_token) def test_initial_sp_call_invalid_response(self): """Send initial SP HTTP request and receive wrong server response.""" self.requests_mock.get(self.FEDERATION_AUTH_URL, text='NON XML RESPONSE') self.assertRaises( exceptions.AuthorizationFailure, self.saml2plugin._send_service_provider_request, self.session) def test_send_authn_req_to_idp(self): self.requests_mock.post(self.IDENTITY_PROVIDER_URL, content=saml2_fixtures.SAML2_ASSERTION) self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL self.saml2plugin.saml2_authn_request = etree.XML( saml2_fixtures.SP_SOAP_RESPONSE) self.saml2plugin._send_idp_saml2_authn_request(self.session) idp_response = etree.tostring( self.saml2plugin.saml2_idp_authn_response) self.assertThat(idp_response, matchers.XMLEquals(saml2_fixtures.SAML2_ASSERTION)) def test_fail_basicauth_idp_authentication(self): self.requests_mock.post(self.IDENTITY_PROVIDER_URL, status_code=401) self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL self.saml2plugin.saml2_authn_request = etree.XML( saml2_fixtures.SP_SOAP_RESPONSE) self.assertRaises( exceptions.Unauthorized, self.saml2plugin._send_idp_saml2_authn_request, self.session) def test_mising_username_password_in_plugin(self): self.assertRaises(TypeError, saml2.V3Saml2Password, self.TEST_URL, self.IDENTITY_PROVIDER, self.IDENTITY_PROVIDER_URL) def test_send_authn_response_to_sp(self): self.requests_mock.post( self.SHIB_CONSUMER_URL, json=saml2_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) self.saml2plugin.relay_state = etree.XML( saml2_fixtures.SP_SOAP_RESPONSE).xpath( self.ECP_RELAY_STATE, namespaces=self.ECP_SAML2_NAMESPACES)[0] self.saml2plugin.saml2_idp_authn_response = etree.XML( saml2_fixtures.SAML2_ASSERTION) self.saml2plugin.idp_response_consumer_url = self.SHIB_CONSUMER_URL self.saml2plugin._send_service_provider_saml2_authn_response( self.session) token_json = self.saml2plugin.authenticated_response.json()['token'] token = self.saml2plugin.authenticated_response.headers[ 'X-Subject-Token'] self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_json) self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, token) def test_consumer_url_mismatch_success(self): self.saml2plugin._check_consumer_urls( self.session, self.SHIB_CONSUMER_URL, self.SHIB_CONSUMER_URL) def test_consumer_url_mismatch(self): self.requests_mock.post(self.SHIB_CONSUMER_URL) invalid_consumer_url = uuid.uuid4().hex self.assertRaises( exceptions.AuthorizationFailure, self.saml2plugin._check_consumer_urls, self.session, self.SHIB_CONSUMER_URL, invalid_consumer_url) def test_custom_302_redirection(self): self.requests_mock.post( self.SHIB_CONSUMER_URL, text='BODY', headers={'location': self.FEDERATION_AUTH_URL}, status_code=302) self.requests_mock.get( self.FEDERATION_AUTH_URL, json=saml2_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) self.session.redirect = False response = self.session.post( self.SHIB_CONSUMER_URL, data='CLIENT BODY') self.assertEqual(302, response.status_code) self.assertEqual(self.FEDERATION_AUTH_URL, response.headers['location']) response = self.saml2plugin._handle_http_ecp_redirect( self.session, response, 'GET') self.assertEqual(self.FEDERATION_AUTH_URL, response.request.url) self.assertEqual('GET', response.request.method) def test_custom_303_redirection(self): self.requests_mock.post( self.SHIB_CONSUMER_URL, text='BODY', headers={'location': self.FEDERATION_AUTH_URL}, status_code=303) self.requests_mock.get( self.FEDERATION_AUTH_URL, json=saml2_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) self.session.redirect = False response = self.session.post( self.SHIB_CONSUMER_URL, data='CLIENT BODY') self.assertEqual(303, response.status_code) self.assertEqual(self.FEDERATION_AUTH_URL, response.headers['location']) response = self.saml2plugin._handle_http_ecp_redirect( self.session, response, 'GET') self.assertEqual(self.FEDERATION_AUTH_URL, response.request.url) self.assertEqual('GET', response.request.method) def test_end_to_end_workflow(self): self.requests_mock.get( self.FEDERATION_AUTH_URL, content=utils.make_oneline(saml2_fixtures.SP_SOAP_RESPONSE)) self.requests_mock.post(self.IDENTITY_PROVIDER_URL, content=saml2_fixtures.SAML2_ASSERTION) self.requests_mock.post( self.SHIB_CONSUMER_URL, json=saml2_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER, 'Content-Type': 'application/json'}) self.session.redirect = False response = self.saml2plugin.get_auth_ref(self.session) self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, response.auth_token) keystoneauth-2.4.1/keystoneauth1/tests/unit/extras/saml2/utils.py000066400000000000000000000021401271245473000252260ustar00rootroot00000000000000# 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 from lxml import etree from keystoneauth1 import session from keystoneauth1.tests.unit import utils ROOTDIR = os.path.dirname(os.path.abspath(__file__)) XMLDIR = os.path.join(ROOTDIR, 'examples', 'xml/') def make_oneline(s): return etree.tostring(etree.XML(s)).replace(b'\n', b'') def _load_xml(filename): with open(XMLDIR + filename, 'rb') as f: return f.read() class TestCase(utils.TestCase): TEST_URL = 'https://keystone:5000/v3' def setUp(self): super(TestCase, self).setUp() self.session = session.Session() keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/000077500000000000000000000000001271245473000230245ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/__init__.py000066400000000000000000000000001271245473000251230ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/test_access.py000066400000000000000000000053211271245473000256770ustar00rootroot00000000000000# 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 uuid from keystoneauth1 import access from keystoneauth1 import fixture from keystoneauth1.identity import access as access_plugin from keystoneauth1 import plugin from keystoneauth1 import session from keystoneauth1.tests.unit import utils class AccessInfoPluginTests(utils.TestCase): def setUp(self): super(AccessInfoPluginTests, self).setUp() self.session = session.Session() self.auth_token = uuid.uuid4().hex def _plugin(self, **kwargs): token = fixture.V3Token() s = token.add_service('identity') s.add_standard_endpoints(public=self.TEST_ROOT_URL) auth_ref = access.create(body=token, auth_token=self.auth_token) return access_plugin.AccessInfoPlugin(auth_ref, **kwargs) def test_auth_ref(self): plugin_obj = self._plugin() self.assertEqual(self.TEST_ROOT_URL, plugin_obj.get_endpoint(self.session, service_type='identity', interface='public')) self.assertEqual(self.auth_token, plugin_obj.get_token(session)) def test_auth_url(self): auth_url = 'http://keystone.test.url' obj = self._plugin(auth_url=auth_url) self.assertEqual(auth_url, obj.get_endpoint(self.session, interface=plugin.AUTH_INTERFACE)) def test_invalidate(self): plugin = self._plugin() auth_ref = plugin.auth_ref self.assertIsInstance(auth_ref, access.AccessInfo) self.assertFalse(plugin.invalidate()) self.assertIs(auth_ref, plugin.auth_ref) def test_project_auth_properties(self): plugin = self._plugin() auth_ref = plugin.auth_ref self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertIsNone(auth_ref.project_id) self.assertIsNone(auth_ref.project_name) def test_domain_auth_properties(self): plugin = self._plugin() auth_ref = plugin.auth_ref self.assertIsNone(auth_ref.domain_id) self.assertIsNone(auth_ref.domain_name) keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/test_identity_common.py000066400000000000000000000453341271245473000276470ustar00rootroot00000000000000# 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 abc import uuid import mock import six from keystoneauth1 import _utils from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1 import identity from keystoneauth1 import plugin from keystoneauth1 import session from keystoneauth1.tests.unit import utils @six.add_metaclass(abc.ABCMeta) class CommonIdentityTests(object): TEST_ROOT_URL = 'http://127.0.0.1:5000/' TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' TEST_COMPUTE_PUBLIC = 'http://nova/novapi/public' TEST_COMPUTE_INTERNAL = 'http://nova/novapi/internal' TEST_COMPUTE_ADMIN = 'http://nova/novapi/admin' TEST_PASS = uuid.uuid4().hex def setUp(self): super(CommonIdentityTests, self).setUp() self.TEST_URL = '%s%s' % (self.TEST_ROOT_URL, self.version) self.TEST_ADMIN_URL = '%s%s' % (self.TEST_ROOT_ADMIN_URL, self.version) self.TEST_DISCOVERY = fixture.DiscoveryList(href=self.TEST_ROOT_URL) self.stub_auth_data() @abc.abstractmethod def create_auth_plugin(self, **kwargs): """Create an auth plugin that makes sense for the auth data. It doesn't really matter what auth mechanism is used but it should be appropriate to the API version. """ @abc.abstractmethod def get_auth_data(self, **kwargs): """Return fake authentication data. This should register a valid token response and ensure that the compute endpoints are set to TEST_COMPUTE_PUBLIC, _INTERNAL and _ADMIN. """ def stub_auth_data(self, **kwargs): token = self.get_auth_data(**kwargs) self.user_id = token.user_id try: self.project_id = token.project_id except AttributeError: self.project_id = token.tenant_id self.stub_auth(json=token) @abc.abstractproperty def version(self): """The API version being tested.""" def test_discovering(self): self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=self.TEST_DISCOVERY) body = 'SUCCESS' # which gives our sample values self.stub_url('GET', ['path'], text=body) a = self.create_auth_plugin() s = session.Session(auth=a) resp = s.get('/path', endpoint_filter={'service_type': 'compute', 'interface': 'admin', 'version': self.version}) self.assertEqual(200, resp.status_code) self.assertEqual(body, resp.text) new_body = 'SC SUCCESS' # if we don't specify a version, we use the URL from the SC self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN, text=new_body) resp = s.get('/path', endpoint_filter={'service_type': 'compute', 'interface': 'admin'}) self.assertEqual(200, resp.status_code) self.assertEqual(new_body, resp.text) def test_discovery_uses_session_cache(self): # register responses such that if the discovery URL is hit more than # once then the response will be invalid and not point to COMPUTE_ADMIN resps = [{'json': self.TEST_DISCOVERY}, {'status_code': 500}] self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) body = 'SUCCESS' self.stub_url('GET', ['path'], text=body) # now either of the two plugins I use, it should not cause a second # request to the discovery url. s = session.Session() a = self.create_auth_plugin() b = self.create_auth_plugin() for auth in (a, b): resp = s.get('/path', auth=auth, endpoint_filter={'service_type': 'compute', 'interface': 'admin', 'version': self.version}) self.assertEqual(200, resp.status_code) self.assertEqual(body, resp.text) def test_discovery_uses_plugin_cache(self): # register responses such that if the discovery URL is hit more than # once then the response will be invalid and not point to COMPUTE_ADMIN resps = [{'json': self.TEST_DISCOVERY}, {'status_code': 500}] self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) body = 'SUCCESS' self.stub_url('GET', ['path'], text=body) # now either of the two sessions I use, it should not cause a second # request to the discovery url. sa = session.Session() sb = session.Session() auth = self.create_auth_plugin() for sess in (sa, sb): resp = sess.get('/path', auth=auth, endpoint_filter={'service_type': 'compute', 'interface': 'admin', 'version': self.version}) self.assertEqual(200, resp.status_code) self.assertEqual(body, resp.text) def test_discovering_with_no_data(self): # which returns discovery information pointing to TEST_URL but there is # no data there. self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, status_code=400) # so the url that will be used is the same TEST_COMPUTE_ADMIN body = 'SUCCESS' self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN, text=body, status_code=200) a = self.create_auth_plugin() s = session.Session(auth=a) resp = s.get('/path', endpoint_filter={'service_type': 'compute', 'interface': 'admin', 'version': self.version}) self.assertEqual(200, resp.status_code) self.assertEqual(body, resp.text) def test_asking_for_auth_endpoint_ignores_checks(self): a = self.create_auth_plugin() s = session.Session(auth=a) auth_url = s.get_endpoint(service_type='compute', interface=plugin.AUTH_INTERFACE) self.assertEqual(self.TEST_URL, auth_url) def _create_expired_auth_plugin(self, **kwargs): expires = _utils.before_utcnow(minutes=20) expired_token = self.get_auth_data(expires=expires) expired_auth_ref = access.create(body=expired_token) body = 'SUCCESS' self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN, text=body) a = self.create_auth_plugin(**kwargs) a.auth_ref = expired_auth_ref return a def test_reauthenticate(self): a = self._create_expired_auth_plugin() expired_auth_ref = a.auth_ref s = session.Session(auth=a) self.assertIsNot(expired_auth_ref, a.get_access(s)) def test_no_reauthenticate(self): a = self._create_expired_auth_plugin(reauthenticate=False) expired_auth_ref = a.auth_ref s = session.Session(auth=a) self.assertIs(expired_auth_ref, a.get_access(s)) def test_invalidate(self): a = self.create_auth_plugin() s = session.Session(auth=a) # trigger token fetching s.get_auth_headers() self.assertTrue(a.auth_ref) self.assertTrue(a.invalidate()) self.assertIsNone(a.auth_ref) self.assertFalse(a.invalidate()) def test_get_auth_properties(self): a = self.create_auth_plugin() s = session.Session() self.assertEqual(self.user_id, a.get_user_id(s)) self.assertEqual(self.project_id, a.get_project_id(s)) def assertAccessInfoEqual(self, a, b): self.assertEqual(a.auth_token, b.auth_token) self.assertEqual(a._data, b._data) def test_check_cache_id_match(self): a = self.create_auth_plugin() b = self.create_auth_plugin() self.assertIsNot(a, b) self.assertIsNone(a.get_auth_state()) self.assertIsNone(b.get_auth_state()) a_id = a.get_cache_id() b_id = b.get_cache_id() self.assertIsNotNone(a_id) self.assertIsNotNone(b_id) self.assertEqual(a_id, b_id) def test_check_cache_id_no_match(self): a = self.create_auth_plugin(project_id='a') b = self.create_auth_plugin(project_id='b') self.assertIsNot(a, b) self.assertIsNone(a.get_auth_state()) self.assertIsNone(b.get_auth_state()) a_id = a.get_cache_id() b_id = b.get_cache_id() self.assertIsNotNone(a_id) self.assertIsNotNone(b_id) self.assertNotEqual(a_id, b_id) def test_get_set_auth_state(self): a = self.create_auth_plugin() b = self.create_auth_plugin() self.assertEqual(a.get_cache_id(), b.get_cache_id()) s = session.Session() a_token = a.get_token(s) self.assertEqual(1, self.requests_mock.call_count) auth_state = a.get_auth_state() self.assertIsNotNone(auth_state) b.set_auth_state(auth_state) b_token = b.get_token(s) self.assertEqual(1, self.requests_mock.call_count) self.assertEqual(a_token, b_token) self.assertAccessInfoEqual(a.auth_ref, b.auth_ref) class V3(CommonIdentityTests, utils.TestCase): @property def version(self): return 'v3' def get_auth_data(self, **kwargs): token = fixture.V3Token(**kwargs) region = 'RegionOne' svc = token.add_service('identity') svc.add_standard_endpoints(admin=self.TEST_ADMIN_URL, region=region) svc = token.add_service('compute') svc.add_standard_endpoints(admin=self.TEST_COMPUTE_ADMIN, public=self.TEST_COMPUTE_PUBLIC, internal=self.TEST_COMPUTE_INTERNAL, region=region) return token def stub_auth(self, subject_token=None, **kwargs): if not subject_token: subject_token = self.TEST_TOKEN kwargs.setdefault('headers', {})['X-Subject-Token'] = subject_token self.stub_url('POST', ['auth', 'tokens'], **kwargs) def create_auth_plugin(self, **kwargs): kwargs.setdefault('auth_url', self.TEST_URL) kwargs.setdefault('username', self.TEST_USER) kwargs.setdefault('password', self.TEST_PASS) return identity.V3Password(**kwargs) class V2(CommonIdentityTests, utils.TestCase): @property def version(self): return 'v2.0' def create_auth_plugin(self, **kwargs): kwargs.setdefault('auth_url', self.TEST_URL) kwargs.setdefault('username', self.TEST_USER) kwargs.setdefault('password', self.TEST_PASS) try: kwargs.setdefault('tenant_id', kwargs.pop('project_id')) except KeyError: pass try: kwargs.setdefault('tenant_name', kwargs.pop('project_name')) except KeyError: pass return identity.V2Password(**kwargs) def get_auth_data(self, **kwargs): token = fixture.V2Token(**kwargs) region = 'RegionOne' svc = token.add_service('identity') svc.add_endpoint(self.TEST_ADMIN_URL, region=region) svc = token.add_service('compute') svc.add_endpoint(public=self.TEST_COMPUTE_PUBLIC, internal=self.TEST_COMPUTE_INTERNAL, admin=self.TEST_COMPUTE_ADMIN, region=region) return token def stub_auth(self, **kwargs): self.stub_url('POST', ['tokens'], **kwargs) class CatalogHackTests(utils.TestCase): TEST_URL = 'http://keystone.server:5000/v2.0' OTHER_URL = 'http://other.server:5000/path' IDENTITY = 'identity' BASE_URL = 'http://keystone.server:5000/' V2_URL = BASE_URL + 'v2.0' V3_URL = BASE_URL + 'v3' def test_getting_endpoints(self): disc = fixture.DiscoveryList(href=self.BASE_URL) self.stub_url('GET', ['/'], base_url=self.BASE_URL, json=disc) token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) endpoint = sess.get_endpoint(service_type=self.IDENTITY, interface='public', version=(3, 0)) self.assertEqual(self.V3_URL, endpoint) def test_returns_original_when_discover_fails(self): token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) self.stub_url('GET', [], base_url=self.BASE_URL, status_code=404) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) endpoint = sess.get_endpoint(service_type=self.IDENTITY, interface='public', version=(3, 0)) self.assertEqual(self.V2_URL, endpoint) def test_getting_endpoints_on_auth_interface(self): disc = fixture.DiscoveryList(href=self.BASE_URL) self.stub_url('GET', ['/'], base_url=self.BASE_URL, status_code=300, json=disc) token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) endpoint = sess.get_endpoint(interface=plugin.AUTH_INTERFACE, version=(3, 0)) self.assertEqual(self.V3_URL, endpoint) class GenericPlugin(plugin.BaseAuthPlugin): BAD_TOKEN = uuid.uuid4().hex def __init__(self): super(GenericPlugin, self).__init__() self.endpoint = 'http://keystone.host:5000' self.headers = {'headerA': 'valueA', 'headerB': 'valueB'} self.cert = '/path/to/cert' self.connection_params = {'cert': self.cert, 'verify': False} def url(self, prefix): return '%s/%s' % (self.endpoint, prefix) def get_token(self, session, **kwargs): # NOTE(jamielennox): by specifying get_headers this should not be used return self.BAD_TOKEN def get_headers(self, session, **kwargs): return self.headers def get_endpoint(self, session, **kwargs): return self.endpoint def get_connection_params(self, session, **kwargs): return self.connection_params class GenericAuthPluginTests(utils.TestCase): # filter doesn't matter to GenericPlugin, but we have to specify one ENDPOINT_FILTER = {uuid.uuid4().hex: uuid.uuid4().hex} def setUp(self): super(GenericAuthPluginTests, self).setUp() self.auth = GenericPlugin() self.session = session.Session(auth=self.auth) def test_setting_headers(self): text = uuid.uuid4().hex self.stub_url('GET', base_url=self.auth.url('prefix'), text=text) resp = self.session.get('prefix', endpoint_filter=self.ENDPOINT_FILTER) self.assertEqual(text, resp.text) for k, v in six.iteritems(self.auth.headers): self.assertRequestHeaderEqual(k, v) self.assertIsNone(self.session.get_token()) self.assertEqual(self.auth.headers, self.session.get_auth_headers()) self.assertNotIn('X-Auth-Token', self.requests_mock.last_request.headers) def test_setting_connection_params(self): text = uuid.uuid4().hex with mock.patch.object(self.session.session, 'request') as mocked: mocked.return_value = utils.TestResponse({'status_code': 200, 'text': text}) resp = self.session.get('prefix', endpoint_filter=self.ENDPOINT_FILTER) self.assertEqual(text, resp.text) # the cert and verify values passed to request are those that were # returned from the auth plugin as connection params. mocked.assert_called_once_with('GET', self.auth.url('prefix'), headers=mock.ANY, allow_redirects=False, cert=self.auth.cert, verify=False) def test_setting_bad_connection_params(self): # The uuid name parameter here is unknown and not in the allowed params # to be returned to the session and so an error will be raised. name = uuid.uuid4().hex self.auth.connection_params[name] = uuid.uuid4().hex e = self.assertRaises(exceptions.UnsupportedParameters, self.session.get, 'prefix', endpoint_filter=self.ENDPOINT_FILTER) self.assertIn(name, str(e)) keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/test_identity_v2.py000066400000000000000000000341141271245473000267000ustar00rootroot00000000000000# 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 copy import json import uuid from keystoneauth1 import _utils as ksa_utils from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1.identity import v2 from keystoneauth1 import session from keystoneauth1.tests.unit import utils class V2IdentityPlugin(utils.TestCase): TEST_ROOT_URL = 'http://127.0.0.1:5000/' TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0') TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0') TEST_PASS = 'password' TEST_SERVICE_CATALOG = [{ "endpoints": [{ "adminURL": "http://cdn.admin-nets.local:8774/v1.0", "region": "RegionOne", "internalURL": "http://127.0.0.1:8774/v1.0", "publicURL": "http://cdn.admin-nets.local:8774/v1.0/" }], "type": "nova_compat", "name": "nova_compat" }, { "endpoints": [{ "adminURL": "http://nova/novapi/admin", "region": "RegionOne", "internalURL": "http://nova/novapi/internal", "publicURL": "http://nova/novapi/public" }], "type": "compute", "name": "nova" }, { "endpoints": [{ "adminURL": "http://glance/glanceapi/admin", "region": "RegionOne", "internalURL": "http://glance/glanceapi/internal", "publicURL": "http://glance/glanceapi/public" }], "type": "image", "name": "glance" }, { "endpoints": [{ "adminURL": TEST_ADMIN_URL, "region": "RegionOne", "internalURL": "http://127.0.0.1:5000/v2.0", "publicURL": "http://127.0.0.1:5000/v2.0" }], "type": "identity", "name": "keystone" }, { "endpoints": [{ "adminURL": "http://swift/swiftapi/admin", "region": "RegionOne", "internalURL": "http://swift/swiftapi/internal", "publicURL": "http://swift/swiftapi/public" }], "type": "object-store", "name": "swift" }] def setUp(self): super(V2IdentityPlugin, self).setUp() self.TEST_RESPONSE_DICT = { "access": { "token": { "expires": "2020-01-01T00:00:10.000123Z", "id": self.TEST_TOKEN, "tenant": { "id": self.TEST_TENANT_ID }, }, "user": { "id": self.TEST_USER }, "serviceCatalog": self.TEST_SERVICE_CATALOG, }, } def stub_auth(self, **kwargs): self.stub_url('POST', ['tokens'], **kwargs) def test_authenticate_with_username_password(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) self.assertIsNone(a.user_id) self.assertFalse(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, 'password': self.TEST_PASS}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_user_id_password(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, user_id=self.TEST_USER, password=self.TEST_PASS) self.assertIsNone(a.username) self.assertFalse(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER, 'password': self.TEST_PASS}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_username_password_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID) self.assertTrue(a.has_scope_parameters) self.assertIsNone(a.user_id) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, 'password': self.TEST_PASS}, 'tenantId': self.TEST_TENANT_ID}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_user_id_password_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, user_id=self.TEST_USER, password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID) self.assertIsNone(a.username) self.assertTrue(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER, 'password': self.TEST_PASS}, 'tenantId': self.TEST_TENANT_ID}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_token(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Token(self.TEST_URL, 'foo') s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'token': {'id': 'foo'}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('x-Auth-Token', 'foo') self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_with_trust_id(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, trust_id='trust') self.assertTrue(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, 'password': self.TEST_PASS}, 'trust_id': 'trust'}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def _do_service_url_test(self, base_url, endpoint_filter): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', ['path'], base_url=base_url, text='SUCCESS', status_code=200) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) resp = s.get('/path', endpoint_filter=endpoint_filter) self.assertEqual(resp.status_code, 200) self.assertEqual(self.requests_mock.last_request.url, base_url + '/path') def test_service_url(self): endpoint_filter = {'service_type': 'compute', 'interface': 'admin', 'service_name': 'nova'} self._do_service_url_test('http://nova/novapi/admin', endpoint_filter) def test_service_url_defaults_to_public(self): endpoint_filter = {'service_type': 'compute'} self._do_service_url_test('http://nova/novapi/public', endpoint_filter) def test_endpoint_filter_without_service_type_fails(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.EndpointNotFound, s.get, '/path', endpoint_filter={'interface': 'admin'}) def test_full_url_overrides_endpoint_filter(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', [], base_url='http://testurl/', text='SUCCESS', status_code=200) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) resp = s.get('http://testurl/', endpoint_filter={'service_type': 'compute'}) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.text, 'SUCCESS') def test_invalid_auth_response_dict(self): self.stub_auth(json={'hello': 'world'}) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', authenticated=True) def test_invalid_auth_response_type(self): self.stub_url('POST', ['tokens'], text='testdata') a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', authenticated=True) def test_invalidate_response(self): resp_data1 = copy.deepcopy(self.TEST_RESPONSE_DICT) resp_data2 = copy.deepcopy(self.TEST_RESPONSE_DICT) resp_data1['access']['token']['id'] = 'token1' resp_data2['access']['token']['id'] = 'token2' auth_responses = [{'json': resp_data1}, {'json': resp_data2}] self.stub_auth(response_list=auth_responses) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertEqual('token1', s.get_token()) self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers()) a.invalidate() self.assertEqual('token2', s.get_token()) self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers()) def test_doesnt_log_password(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) password = uuid.uuid4().hex a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=password) s = session.Session(auth=a) self.assertEqual(self.TEST_TOKEN, s.get_token()) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) self.assertNotIn(password, self.logger.output) def test_password_with_no_user_id_or_name(self): self.assertRaises(TypeError, v2.Password, self.TEST_URL, password=self.TEST_PASS) def test_password_cache_id(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) trust_id = uuid.uuid4().hex a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, trust_id=trust_id) b = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, trust_id=trust_id) a_id = a.get_cache_id() b_id = b.get_cache_id() self.assertEqual(a_id, b_id) c = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, tenant_id=trust_id) # same value different param c_id = c.get_cache_id() self.assertNotEqual(a_id, c_id) self.assertIsNone(a.get_auth_state()) self.assertIsNone(b.get_auth_state()) self.assertIsNone(c.get_auth_state()) s = session.Session() self.assertEqual(self.TEST_TOKEN, a.get_token(s)) self.assertTrue(self.requests_mock.called) def test_password_change_auth_state(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) expired = ksa_utils.before_utcnow(days=2) token = fixture.V2Token(expires=expired) auth_ref = access.create(body=token) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, tenant_id=uuid.uuid4().hex) initial_cache_id = a.get_cache_id() state = a.get_auth_state() self.assertIsNone(state) state = json.dumps({'auth_token': auth_ref.auth_token, 'body': auth_ref._data}) a.set_auth_state(state) self.assertEqual(token.token_id, a.auth_ref.auth_token) s = session.Session() self.assertEqual(self.TEST_TOKEN, a.get_token(s)) # updates expired self.assertEqual(initial_cache_id, a.get_cache_id()) keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/test_identity_v3.py000066400000000000000000000571501271245473000267060ustar00rootroot00000000000000# 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 copy import json import uuid from keystoneauth1 import _utils as ksa_utils from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import base as v3_base from keystoneauth1 import session from keystoneauth1.tests.unit import utils class V3IdentityPlugin(utils.TestCase): TEST_ROOT_URL = 'http://127.0.0.1:5000/' TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') TEST_PASS = 'password' TEST_SERVICE_CATALOG = [{ "endpoints": [{ "url": "http://cdn.admin-nets.local:8774/v1.0/", "region": "RegionOne", "interface": "public" }, { "url": "http://127.0.0.1:8774/v1.0", "region": "RegionOne", "interface": "internal" }, { "url": "http://cdn.admin-nets.local:8774/v1.0", "region": "RegionOne", "interface": "admin" }], "type": "nova_compat" }, { "endpoints": [{ "url": "http://nova/novapi/public", "region": "RegionOne", "interface": "public" }, { "url": "http://nova/novapi/internal", "region": "RegionOne", "interface": "internal" }, { "url": "http://nova/novapi/admin", "region": "RegionOne", "interface": "admin" }], "type": "compute", "name": "nova", }, { "endpoints": [{ "url": "http://glance/glanceapi/public", "region": "RegionOne", "interface": "public" }, { "url": "http://glance/glanceapi/internal", "region": "RegionOne", "interface": "internal" }, { "url": "http://glance/glanceapi/admin", "region": "RegionOne", "interface": "admin" }], "type": "image", "name": "glance" }, { "endpoints": [{ "url": "http://127.0.0.1:5000/v3", "region": "RegionOne", "interface": "public" }, { "url": "http://127.0.0.1:5000/v3", "region": "RegionOne", "interface": "internal" }, { "url": TEST_ADMIN_URL, "region": "RegionOne", "interface": "admin" }], "type": "identity" }, { "endpoints": [{ "url": "http://swift/swiftapi/public", "region": "RegionOne", "interface": "public" }, { "url": "http://swift/swiftapi/internal", "region": "RegionOne", "interface": "internal" }, { "url": "http://swift/swiftapi/admin", "region": "RegionOne", "interface": "admin" }], "type": "object-store" }] TEST_SERVICE_PROVIDERS = [ { "auth_url": "https://sp1.com/v3/OS-FEDERATION/" "identity_providers/acme/protocols/saml2/auth", "id": "sp1", "sp_url": "https://sp1.com/Shibboleth.sso/SAML2/ECP" }, { "auth_url": "https://sp2.com/v3/OS-FEDERATION/" "identity_providers/acme/protocols/saml2/auth", "id": "sp2", "sp_url": "https://sp2.com/Shibboleth.sso/SAML2/ECP" } ] def setUp(self): super(V3IdentityPlugin, self).setUp() self.TEST_DISCOVERY_RESPONSE = { 'versions': {'values': [fixture.V3Discovery(self.TEST_URL)]}} self.TEST_RESPONSE_DICT = { "token": { "methods": [ "token", "password" ], "expires_at": "2020-01-01T00:00:10.000123Z", "project": { "domain": { "id": self.TEST_DOMAIN_ID, "name": self.TEST_DOMAIN_NAME }, "id": self.TEST_TENANT_ID, "name": self.TEST_TENANT_NAME }, "user": { "domain": { "id": self.TEST_DOMAIN_ID, "name": self.TEST_DOMAIN_NAME }, "id": self.TEST_USER, "name": self.TEST_USER }, "issued_at": "2013-05-29T16:55:21.468960Z", "catalog": self.TEST_SERVICE_CATALOG, "service_providers": self.TEST_SERVICE_PROVIDERS }, } self.TEST_PROJECTS_RESPONSE = { "projects": [ { "domain_id": "1789d1", "enabled": "True", "id": "263fd9", "links": { "self": "https://identity:5000/v3/projects/263fd9" }, "name": "Dev Group A" }, { "domain_id": "1789d1", "enabled": "True", "id": "e56ad3", "links": { "self": "https://identity:5000/v3/projects/e56ad3" }, "name": "Dev Group B" } ], "links": { "self": "https://identity:5000/v3/projects", } } def stub_auth(self, subject_token=None, **kwargs): if not subject_token: subject_token = self.TEST_TOKEN self.stub_url('POST', ['auth', 'tokens'], headers={'X-Subject-Token': subject_token}, **kwargs) def test_authenticate_with_username_password(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) self.assertFalse(a.has_scope_parameters) s = session.Session(auth=a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_username_password_domain_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, domain_id=self.TEST_DOMAIN_ID) self.assertTrue(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}}, 'scope': {'domain': {'id': self.TEST_DOMAIN_ID}}}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_username_password_project_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, project_id=self.TEST_TENANT_ID) self.assertTrue(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}}, 'scope': {'project': {'id': self.TEST_TENANT_ID}}}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) self.assertEqual(s.auth.auth_ref.project_id, self.TEST_TENANT_ID) def test_authenticate_with_token(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Token(self.TEST_URL, self.TEST_TOKEN) s = session.Session(auth=a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['token'], 'token': {'id': self.TEST_TOKEN}}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_with_expired(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) d = copy.deepcopy(self.TEST_RESPONSE_DICT) d['token']['expires_at'] = '2000-01-01T00:00:10.000123Z' a = v3.Password(self.TEST_URL, username='username', password='password') a.auth_ref = access.create(body=d) s = session.Session(auth=a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) self.assertEqual(a.auth_ref._data['token']['expires_at'], self.TEST_RESPONSE_DICT['token']['expires_at']) def test_with_domain_and_project_scoping(self): a = v3.Password(self.TEST_URL, username='username', password='password', project_id='project', domain_id='domain') self.assertTrue(a.has_scope_parameters) self.assertRaises(exceptions.AuthorizationFailure, a.get_token, None) self.assertRaises(exceptions.AuthorizationFailure, a.get_headers, None) def test_with_trust_id(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, trust_id='trust') self.assertTrue(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}}, 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_with_multiple_mechanisms_factory(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) p = v3.PasswordMethod(username=self.TEST_USER, password=self.TEST_PASS) t = v3.TokenMethod(token='foo') a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust') s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password', 'token'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}, 'token': {'id': 'foo'}}, 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_with_multiple_mechanisms(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) p = v3.PasswordMethod(username=self.TEST_USER, password=self.TEST_PASS) t = v3.TokenMethod(token='foo') a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust') self.assertTrue(a.has_scope_parameters) s = session.Session(auth=a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password', 'token'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}, 'token': {'id': 'foo'}}, 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_with_multiple_scopes(self): s = session.Session() a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, domain_id='x', project_id='x') self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, domain_id='x', trust_id='x') self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) def _do_service_url_test(self, base_url, endpoint_filter): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', ['path'], base_url=base_url, text='SUCCESS', status_code=200) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) resp = s.get('/path', endpoint_filter=endpoint_filter) self.assertEqual(resp.status_code, 200) self.assertEqual(self.requests_mock.last_request.url, base_url + '/path') def test_service_url(self): endpoint_filter = {'service_type': 'compute', 'interface': 'admin', 'service_name': 'nova'} self._do_service_url_test('http://nova/novapi/admin', endpoint_filter) def test_service_url_defaults_to_public(self): endpoint_filter = {'service_type': 'compute'} self._do_service_url_test('http://nova/novapi/public', endpoint_filter) def test_endpoint_filter_without_service_type_fails(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.EndpointNotFound, s.get, '/path', endpoint_filter={'interface': 'admin'}) def test_full_url_overrides_endpoint_filter(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', [], base_url='http://testurl/', text='SUCCESS', status_code=200) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) resp = s.get('http://testurl/', endpoint_filter={'service_type': 'compute'}) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.text, 'SUCCESS') def test_service_providers_urls(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session() auth_ref = a.get_auth_ref(s) service_providers = auth_ref.service_providers self.assertEqual('https://sp1.com/v3/OS-FEDERATION/' 'identity_providers/acme/protocols/saml2/auth', service_providers.get_auth_url('sp1')) self.assertEqual('https://sp1.com/Shibboleth.sso/SAML2/ECP', service_providers.get_sp_url('sp1')) self.assertEqual('https://sp2.com/v3/OS-FEDERATION/' 'identity_providers/acme/protocols/saml2/auth', service_providers.get_auth_url('sp2')) self.assertEqual('https://sp2.com/Shibboleth.sso/SAML2/ECP', service_providers.get_sp_url('sp2')) def test_handle_missing_service_provider(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session() auth_ref = a.get_auth_ref(s) service_providers = auth_ref.service_providers self.assertRaises(exceptions.ServiceProviderNotFound, service_providers._get_service_provider, uuid.uuid4().hex) def test_invalid_auth_response_dict(self): self.stub_auth(json={'hello': 'world'}) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', authenticated=True) def test_invalid_auth_response_type(self): self.stub_url('POST', ['auth', 'tokens'], text='testdata') a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', authenticated=True) def test_invalidate_response(self): auth_responses = [{'status_code': 200, 'json': self.TEST_RESPONSE_DICT, 'headers': {'X-Subject-Token': 'token1'}}, {'status_code': 200, 'json': self.TEST_RESPONSE_DICT, 'headers': {'X-Subject-Token': 'token2'}}] self.requests_mock.post('%s/auth/tokens' % self.TEST_URL, auth_responses) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertEqual('token1', s.get_token()) self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers()) a.invalidate() self.assertEqual('token2', s.get_token()) self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers()) def test_doesnt_log_password(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) password = uuid.uuid4().hex a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=password) s = session.Session(a) self.assertEqual(self.TEST_TOKEN, s.get_token()) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) self.assertNotIn(password, self.logger.output) def test_sends_nocatalog(self): del self.TEST_RESPONSE_DICT['token']['catalog'] self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, include_catalog=False) s = session.Session(auth=a) s.get_token() auth_url = self.TEST_URL + '/auth/tokens' self.assertEqual(auth_url, a.token_url) self.assertEqual(auth_url + '?nocatalog', self.requests_mock.last_request.url) def test_symbols(self): self.assertIs(v3.AuthMethod, v3_base.AuthMethod) self.assertIs(v3.AuthConstructor, v3_base.AuthConstructor) self.assertIs(v3.Auth, v3_base.Auth) def test_unscoped_request(self): token = fixture.V3Token() self.stub_auth(json=token) password = uuid.uuid4().hex a = v3.Password(self.TEST_URL, user_id=token.user_id, password=password, unscoped=True) s = session.Session() auth_ref = a.get_access(s) self.assertFalse(auth_ref.scoped) body = self.requests_mock.last_request.json() ident = body['auth']['identity'] self.assertEqual(['password'], ident['methods']) self.assertEqual(token.user_id, ident['password']['user']['id']) self.assertEqual(password, ident['password']['user']['password']) self.assertEqual({}, body['auth']['scope']['unscoped']) def test_unscoped_with_scope_data(self): a = v3.Password(self.TEST_URL, user_id=uuid.uuid4().hex, password=uuid.uuid4().hex, unscoped=True, project_id=uuid.uuid4().hex) s = session.Session() self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) def test_password_cache_id(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) project_name = uuid.uuid4().hex a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id=self.TEST_DOMAIN_ID, project_domain_name=self.TEST_DOMAIN_NAME, project_name=project_name) b = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id=self.TEST_DOMAIN_ID, project_domain_name=self.TEST_DOMAIN_NAME, project_name=project_name) a_id = a.get_cache_id() b_id = b.get_cache_id() self.assertEqual(a_id, b_id) c = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id=self.TEST_DOMAIN_ID, project_domain_name=self.TEST_DOMAIN_NAME, project_id=project_name) # same value different param c_id = c.get_cache_id() self.assertNotEqual(a_id, c_id) self.assertIsNone(a.get_auth_state()) self.assertIsNone(b.get_auth_state()) self.assertIsNone(c.get_auth_state()) s = session.Session() self.assertEqual(self.TEST_TOKEN, a.get_token(s)) self.assertTrue(self.requests_mock.called) def test_password_change_auth_state(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) expired = ksa_utils.before_utcnow(days=2) token = fixture.V3Token(expires=expired) token_id = uuid.uuid4().hex state = json.dumps({'auth_token': token_id, 'body': token}) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id=self.TEST_DOMAIN_ID, project_id=uuid.uuid4().hex) initial_cache_id = a.get_cache_id() self.assertIsNone(a.get_auth_state()) a.set_auth_state(state) self.assertEqual(token_id, a.auth_ref.auth_token) s = session.Session() self.assertEqual(self.TEST_TOKEN, a.get_token(s)) # updates expired self.assertEqual(initial_cache_id, a.get_cache_id()) keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/test_identity_v3_federation.py000066400000000000000000000244011271245473000310770ustar00rootroot00000000000000# 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 copy import uuid import six from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1 import identity from keystoneauth1.identity import v3 from keystoneauth1 import session from keystoneauth1.tests.unit import k2k_fixtures from keystoneauth1.tests.unit import utils class TesterFederationPlugin(v3.FederationBaseAuth): def get_unscoped_auth_ref(self, sess, **kwargs): # This would go and talk to an idp or something resp = sess.post(self.federated_token_url, authenticated=False) return access.create(resp=resp) class V3FederatedPlugin(utils.TestCase): AUTH_URL = 'http://keystone/v3' def setUp(self): super(V3FederatedPlugin, self).setUp() self.unscoped_token = fixture.V3Token() self.unscoped_token_id = uuid.uuid4().hex self.scoped_token = copy.deepcopy(self.unscoped_token) self.scoped_token.set_project_scope() self.scoped_token.methods.append('token') self.scoped_token_id = uuid.uuid4().hex s = self.scoped_token.add_service('compute', name='nova') s.add_standard_endpoints(public='http://nova/public', admin='http://nova/admin', internal='http://nova/internal') self.idp = uuid.uuid4().hex self.protocol = uuid.uuid4().hex self.token_url = ('%s/OS-FEDERATION/identity_providers/%s/protocols/%s' '/auth' % (self.AUTH_URL, self.idp, self.protocol)) headers = {'X-Subject-Token': self.unscoped_token_id} self.unscoped_mock = self.requests_mock.post(self.token_url, json=self.unscoped_token, headers=headers) headers = {'X-Subject-Token': self.scoped_token_id} auth_url = self.AUTH_URL + '/auth/tokens' self.scoped_mock = self.requests_mock.post(auth_url, json=self.scoped_token, headers=headers) def get_plugin(self, **kwargs): kwargs.setdefault('auth_url', self.AUTH_URL) kwargs.setdefault('protocol', self.protocol) kwargs.setdefault('identity_provider', self.idp) return TesterFederationPlugin(**kwargs) def test_federated_url(self): plugin = self.get_plugin() self.assertEqual(self.token_url, plugin.federated_token_url) def test_unscoped_behaviour(self): sess = session.Session(auth=self.get_plugin()) self.assertEqual(self.unscoped_token_id, sess.get_token()) self.assertTrue(self.unscoped_mock.called) self.assertFalse(self.scoped_mock.called) def test_scoped_behaviour(self): auth = self.get_plugin(project_id=self.scoped_token.project_id) sess = session.Session(auth=auth) self.assertEqual(self.scoped_token_id, sess.get_token()) self.assertTrue(self.unscoped_mock.called) self.assertTrue(self.scoped_mock.called) class K2KAuthPluginTest(utils.TestCase): TEST_ROOT_URL = 'http://127.0.0.1:5000/' TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') TEST_PASS = 'password' REQUEST_ECP_URL = TEST_URL + '/auth/OS-FEDERATION/saml2/ecp' SP_ROOT_URL = 'https://sp1.com/v3' SP_ID = 'sp1' SP_URL = 'https://sp1.com/Shibboleth.sso/SAML2/ECP' SP_AUTH_URL = (SP_ROOT_URL + '/OS-FEDERATION/identity_providers' '/testidp/protocols/saml2/auth') SERVICE_PROVIDER_DICT = { 'id': SP_ID, 'auth_url': SP_AUTH_URL, 'sp_url': SP_URL } def setUp(self): super(K2KAuthPluginTest, self).setUp() self.token_v3 = fixture.V3Token() self.token_v3.add_service_provider( self.SP_ID, self.SP_AUTH_URL, self.SP_URL) self.session = session.Session() self.k2kplugin = self.get_plugin() def _get_base_plugin(self): self.stub_url('POST', ['auth', 'tokens'], headers={'X-Subject-Token': uuid.uuid4().hex}, json=self.token_v3) return v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) def _mock_k2k_flow_urls(self): # List versions available for auth self.requests_mock.get( self.TEST_URL, json={'version': fixture.V3Discovery(self.TEST_URL)}, headers={'Content-Type': 'application/json'}) # The IdP should return a ECP wrapped assertion when requested self.requests_mock.register_uri( 'POST', self.REQUEST_ECP_URL, content=six.b(k2k_fixtures.ECP_ENVELOPE), headers={'Content-Type': 'application/vnd.paos+xml'}, status_code=200) # The SP should respond with a 302 self.requests_mock.register_uri( 'POST', self.SP_URL, content=six.b(k2k_fixtures.TOKEN_BASED_ECP), headers={'Content-Type': 'application/vnd.paos+xml'}, status_code=302) # Should not follow the redirect URL, but use the auth_url attribute self.requests_mock.register_uri( 'GET', self.SP_AUTH_URL, json=k2k_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': k2k_fixtures.UNSCOPED_TOKEN_HEADER}) def get_plugin(self, **kwargs): kwargs.setdefault('base_plugin', self._get_base_plugin()) kwargs.setdefault('service_provider', self.SP_ID) return v3.Keystone2Keystone(**kwargs) def test_remote_url(self): remote_auth_url = self.k2kplugin._remote_auth_url(self.SP_AUTH_URL) self.assertEqual(self.SP_ROOT_URL, remote_auth_url) def test_fail_getting_ecp_assertion(self): self.requests_mock.get( self.TEST_URL, json={'version': fixture.V3Discovery(self.TEST_URL)}, headers={'Content-Type': 'application/json'}) self.requests_mock.register_uri( 'POST', self.REQUEST_ECP_URL, status_code=401) self.assertRaises(exceptions.AuthorizationFailure, self.k2kplugin._get_ecp_assertion, self.session) def test_get_ecp_assertion_empty_response(self): self.requests_mock.get( self.TEST_URL, json={'version': fixture.V3Discovery(self.TEST_URL)}, headers={'Content-Type': 'application/json'}) self.requests_mock.register_uri( 'POST', self.REQUEST_ECP_URL, headers={'Content-Type': 'application/vnd.paos+xml'}, content=six.b(''), status_code=200) self.assertRaises(exceptions.InvalidResponse, self.k2kplugin._get_ecp_assertion, self.session) def test_get_ecp_assertion_wrong_headers(self): self.requests_mock.get( self.TEST_URL, json={'version': fixture.V3Discovery(self.TEST_URL)}, headers={'Content-Type': 'application/json'}) self.requests_mock.register_uri( 'POST', self.REQUEST_ECP_URL, headers={'Content-Type': uuid.uuid4().hex}, content=six.b(''), status_code=200) self.assertRaises(exceptions.InvalidResponse, self.k2kplugin._get_ecp_assertion, self.session) def test_send_ecp_authn_response(self): self._mock_k2k_flow_urls() # Perform the request response = self.k2kplugin._send_service_provider_ecp_authn_response( self.session, self.SP_URL, self.SP_AUTH_URL) # Check the response self.assertEqual(k2k_fixtures.UNSCOPED_TOKEN_HEADER, response.headers['X-Subject-Token']) def test_end_to_end_workflow(self): self._mock_k2k_flow_urls() auth_ref = self.k2kplugin.get_auth_ref(self.session) self.assertEqual(k2k_fixtures.UNSCOPED_TOKEN_HEADER, auth_ref.auth_token) def test_end_to_end_with_generic_password(self): # List versions available for auth self.requests_mock.get( self.TEST_ROOT_URL, json=fixture.DiscoveryList(self.TEST_ROOT_URL), headers={'Content-Type': 'application/json'}) # The IdP should return a ECP wrapped assertion when requested self.requests_mock.register_uri( 'POST', self.REQUEST_ECP_URL, content=six.b(k2k_fixtures.ECP_ENVELOPE), headers={'Content-Type': 'application/vnd.paos+xml'}, status_code=200) # The SP should respond with a 302 self.requests_mock.register_uri( 'POST', self.SP_URL, content=six.b(k2k_fixtures.TOKEN_BASED_ECP), headers={'Content-Type': 'application/vnd.paos+xml'}, status_code=302) # Should not follow the redirect URL, but use the auth_url attribute self.requests_mock.register_uri( 'GET', self.SP_AUTH_URL, json=k2k_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': k2k_fixtures.UNSCOPED_TOKEN_HEADER}) self.stub_url('POST', ['auth', 'tokens'], headers={'X-Subject-Token': uuid.uuid4().hex}, json=self.token_v3) plugin = identity.Password(self.TEST_ROOT_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id='default') k2kplugin = self.get_plugin(base_plugin=plugin) self.assertEqual(k2k_fixtures.UNSCOPED_TOKEN_HEADER, k2kplugin.get_token(self.session)) keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/test_identity_v3_oidc.py000066400000000000000000000145451271245473000277050ustar00rootroot00000000000000# 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 uuid from six.moves import urllib from keystoneauth1.identity.v3 import oidc from keystoneauth1 import session from keystoneauth1.tests.unit import oidc_fixtures from keystoneauth1.tests.unit import utils KEYSTONE_TOKEN_VALUE = uuid.uuid4().hex class AuthenticateOIDCTests(utils.TestCase): def setUp(self): super(AuthenticateOIDCTests, self).setUp() self.session = session.Session() self.AUTH_URL = 'http://keystone:5000/v3' self.IDENTITY_PROVIDER = 'bluepages' self.PROTOCOL = 'oidc' self.USER_NAME = 'oidc_user@example.com' self.PASSWORD = uuid.uuid4().hex self.CLIENT_ID = uuid.uuid4().hex self.CLIENT_SECRET = uuid.uuid4().hex self.ACCESS_TOKEN_ENDPOINT = 'https://localhost:8020/oidc/token' self.FEDERATION_AUTH_URL = '%s/%s' % ( self.AUTH_URL, 'OS-FEDERATION/identity_providers/bluepages/protocols/oidc/auth') self.REDIRECT_URL = 'urn:ietf:wg:oauth:2.0:oob' self.CODE = '4/M9TNz2G9WVwYxSjx0w9AgA1bOmryJltQvOhQMq0czJs.cnLNVAfqwG' self.oidc_password = oidc.OidcPassword( self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, access_token_endpoint=self.ACCESS_TOKEN_ENDPOINT, username=self.USER_NAME, password=self.PASSWORD) self.oidc_grant = oidc.OidcAuthorizationCode( self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, access_token_endpoint=self.ACCESS_TOKEN_ENDPOINT, redirect_uri=self.REDIRECT_URL, code=self.CODE) class OIDCPasswordTests(AuthenticateOIDCTests): def test_initial_call_to_get_access_token(self): """Test initial call, expect JSON access token.""" # Mock the output that creates the access token self.requests_mock.post( self.ACCESS_TOKEN_ENDPOINT, json=oidc_fixtures.ACCESS_TOKEN_VIA_PASSWORD_RESP) # Prep all the values and send the request grant_type = 'password' scope = 'profile email' client_auth = (self.CLIENT_ID, self.CLIENT_SECRET) payload = {'grant_type': grant_type, 'username': self.USER_NAME, 'password': self.PASSWORD, 'scope': scope} res = self.oidc_password._get_access_token(self.session, client_auth, payload, self.ACCESS_TOKEN_ENDPOINT) # Verify the request matches the expected structure self.assertEqual(self.ACCESS_TOKEN_ENDPOINT, res.request.url) self.assertEqual('POST', res.request.method) encoded_payload = urllib.parse.urlencode(payload) self.assertEqual(encoded_payload, res.request.body) def test_second_call_to_protected_url(self): """Test subsequent call, expect Keystone token.""" # Mock the output that creates the keystone token self.requests_mock.post( self.FEDERATION_AUTH_URL, json=oidc_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) # Prep all the values and send the request access_token = uuid.uuid4().hex headers = {'Authorization': 'Bearer ' + access_token} res = self.oidc_password._get_keystone_token(self.session, headers, self.FEDERATION_AUTH_URL) # Verify the request matches the expected structure self.assertEqual(self.FEDERATION_AUTH_URL, res.request.url) self.assertEqual('POST', res.request.method) self.assertEqual(headers['Authorization'], res.request.headers['Authorization']) def test_end_to_end_workflow(self): """Test full OpenID Connect workflow.""" # Mock the output that creates the access token self.requests_mock.post( self.ACCESS_TOKEN_ENDPOINT, json=oidc_fixtures.ACCESS_TOKEN_VIA_PASSWORD_RESP) # Mock the output that creates the keystone token self.requests_mock.post( self.FEDERATION_AUTH_URL, json=oidc_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) response = self.oidc_password.get_unscoped_auth_ref(self.session) self.assertEqual(KEYSTONE_TOKEN_VALUE, response.auth_token) class OIDCAuthorizationGrantTests(AuthenticateOIDCTests): def test_initial_call_to_get_access_token(self): """Test initial call, expect JSON access token.""" # Mock the output that creates the access token self.requests_mock.post( self.ACCESS_TOKEN_ENDPOINT, json=oidc_fixtures.ACCESS_TOKEN_VIA_AUTH_GRANT_RESP) # Prep all the values and send the request grant_type = 'authorization_code' client_auth = (self.CLIENT_ID, self.CLIENT_SECRET) payload = {'grant_type': grant_type, 'redirect_uri': self.REDIRECT_URL, 'code': self.CODE} res = self.oidc_grant._get_access_token(self.session, client_auth, payload, self.ACCESS_TOKEN_ENDPOINT) # Verify the request matches the expected structure self.assertEqual(self.ACCESS_TOKEN_ENDPOINT, res.request.url) self.assertEqual('POST', res.request.method) encoded_payload = urllib.parse.urlencode(payload) self.assertEqual(encoded_payload, res.request.body) keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/test_password.py000066400000000000000000000053141271245473000263020ustar00rootroot00000000000000# 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 uuid from keystoneauth1.identity.generic import password from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import password as v3_password from keystoneauth1.tests.unit.identity import utils class PasswordTests(utils.GenericPluginTestCase): PLUGIN_CLASS = password.Password V2_PLUGIN_CLASS = v2.Password V3_PLUGIN_CLASS = v3.Password def new_plugin(self, **kwargs): kwargs.setdefault('username', uuid.uuid4().hex) kwargs.setdefault('password', uuid.uuid4().hex) return super(PasswordTests, self).new_plugin(**kwargs) def test_with_user_domain_params(self): self.stub_discovery() self.assertCreateV3(domain_id=uuid.uuid4().hex, user_domain_id=uuid.uuid4().hex) def test_v3_user_params_v2_url(self): self.stub_discovery(v3=False) self.assertDiscoveryFailure(user_domain_id=uuid.uuid4().hex) def test_v3_domain_params_v2_url(self): self.stub_discovery(v3=False) self.assertDiscoveryFailure(domain_id=uuid.uuid4().hex) def test_v3_disocovery_failure_v2_url(self): auth_url = self.TEST_URL + 'v2.0' self.stub_url('GET', json={}, base_url='/v2.0', status_code=500) self.assertDiscoveryFailure(domain_id=uuid.uuid4().hex, auth_url=auth_url) def test_symbols(self): self.assertIs(v3.Password, v3_password.Password) self.assertIs(v3.PasswordMethod, v3_password.PasswordMethod) def test_default_domain_id_with_v3(self): default_domain_id = uuid.uuid4().hex p = super(PasswordTests, self).test_default_domain_id_with_v3( default_domain_id=default_domain_id) self.assertEqual(default_domain_id, p._plugin.auth_methods[0].user_domain_id) def test_default_domain_name_with_v3(self): default_domain_name = uuid.uuid4().hex p = super(PasswordTests, self).test_default_domain_name_with_v3( default_domain_name=default_domain_name) self.assertEqual(default_domain_name, p._plugin.auth_methods[0].user_domain_name) keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/test_token.py000066400000000000000000000023061271245473000255560ustar00rootroot00000000000000# 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 uuid from keystoneauth1.identity.generic import token from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import token as v3_token from keystoneauth1.tests.unit.identity import utils class TokenTests(utils.GenericPluginTestCase): PLUGIN_CLASS = token.Token V2_PLUGIN_CLASS = v2.Token V3_PLUGIN_CLASS = v3.Token def new_plugin(self, **kwargs): kwargs.setdefault('token', uuid.uuid4().hex) return super(TokenTests, self).new_plugin(**kwargs) def test_symbols(self): self.assertIs(v3.Token, v3_token.Token) self.assertIs(v3.TokenMethod, v3_token.TokenMethod) keystoneauth-2.4.1/keystoneauth1/tests/unit/identity/utils.py000066400000000000000000000145601271245473000245440ustar00rootroot00000000000000# 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 uuid from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1 import session from keystoneauth1.tests.unit import utils class GenericPluginTestCase(utils.TestCase): TEST_URL = 'http://keystone.host:5000/' # OVERRIDE THESE IN SUB CLASSES PLUGIN_CLASS = None V2_PLUGIN_CLASS = None V3_PLUGIN_CLASS = None def setUp(self): super(GenericPluginTestCase, self).setUp() self.token_v2 = fixture.V2Token() self.token_v3 = fixture.V3Token() self.token_v3_id = uuid.uuid4().hex self.session = session.Session() self.stub_url('POST', ['v2.0', 'tokens'], json=self.token_v2) self.stub_url('POST', ['v3', 'auth', 'tokens'], headers={'X-Subject-Token': self.token_v3_id}, json=self.token_v3) def new_plugin(self, **kwargs): kwargs.setdefault('auth_url', self.TEST_URL) return self.PLUGIN_CLASS(**kwargs) def stub_discovery(self, base_url=None, **kwargs): kwargs.setdefault('href', self.TEST_URL) disc = fixture.DiscoveryList(**kwargs) self.stub_url('GET', json=disc, base_url=base_url, status_code=300) return disc def assertCreateV3(self, **kwargs): auth = self.new_plugin(**kwargs) auth_ref = auth.get_auth_ref(self.session) self.assertIsInstance(auth_ref, access.AccessInfoV3) self.assertEqual(self.TEST_URL + 'v3/auth/tokens', self.requests_mock.last_request.url) self.assertIsInstance(auth._plugin, self.V3_PLUGIN_CLASS) return auth def assertCreateV2(self, **kwargs): auth = self.new_plugin(**kwargs) auth_ref = auth.get_auth_ref(self.session) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertEqual(self.TEST_URL + 'v2.0/tokens', self.requests_mock.last_request.url) self.assertIsInstance(auth._plugin, self.V2_PLUGIN_CLASS) return auth def assertDiscoveryFailure(self, **kwargs): plugin = self.new_plugin(**kwargs) self.assertRaises(exceptions.DiscoveryFailure, plugin.get_auth_ref, self.session) def test_create_v3_if_domain_params(self): self.stub_discovery() self.assertCreateV3(domain_id=uuid.uuid4().hex) self.assertCreateV3(domain_name=uuid.uuid4().hex) self.assertCreateV3(project_name=uuid.uuid4().hex, project_domain_name=uuid.uuid4().hex) self.assertCreateV3(project_name=uuid.uuid4().hex, project_domain_id=uuid.uuid4().hex) def test_create_v2_if_no_domain_params(self): self.stub_discovery() self.assertCreateV2() self.assertCreateV2(project_id=uuid.uuid4().hex) self.assertCreateV2(project_name=uuid.uuid4().hex) self.assertCreateV2(tenant_id=uuid.uuid4().hex) self.assertCreateV2(tenant_name=uuid.uuid4().hex) def test_v3_params_v2_url(self): self.stub_discovery(v3=False) self.assertDiscoveryFailure(domain_name=uuid.uuid4().hex) def test_v2_params_v3_url(self): self.stub_discovery(v2=False) self.assertCreateV3() def test_no_urls(self): self.stub_discovery(v2=False, v3=False) self.assertDiscoveryFailure() def test_path_based_url_v2(self): self.stub_url('GET', ['v2.0'], status_code=403) self.assertCreateV2(auth_url=self.TEST_URL + 'v2.0') def test_path_based_url_v3(self): self.stub_url('GET', ['v3'], status_code=403) self.assertCreateV3(auth_url=self.TEST_URL + 'v3') def test_disc_error_for_failure(self): self.stub_url('GET', [], status_code=403) self.assertDiscoveryFailure() def test_v3_plugin_from_failure(self): url = self.TEST_URL + 'v3' self.stub_url('GET', [], base_url=url, status_code=403) self.assertCreateV3(auth_url=url) def test_unknown_discovery_version(self): # make a v4 entry that's mostly the same as a v3 self.stub_discovery(v2=False, v3_id='v4.0') self.assertDiscoveryFailure() def test_default_domain_id_with_v3(self, **kwargs): self.stub_discovery() project_name = uuid.uuid4().hex default_domain_id = kwargs.setdefault('default_domain_id', uuid.uuid4().hex) p = self.assertCreateV3(project_name=project_name, **kwargs) self.assertEqual(default_domain_id, p._plugin.project_domain_id) self.assertEqual(project_name, p._plugin.project_name) return p def test_default_domain_id_no_v3(self): self.stub_discovery(v3=False) project_name = uuid.uuid4().hex default_domain_id = uuid.uuid4().hex p = self.assertCreateV2(project_name=project_name, default_domain_id=default_domain_id) self.assertEqual(project_name, p._plugin.tenant_name) def test_default_domain_name_with_v3(self, **kwargs): self.stub_discovery() project_name = uuid.uuid4().hex default_domain_name = kwargs.setdefault('default_domain_name', uuid.uuid4().hex) p = self.assertCreateV3(project_name=project_name, **kwargs) self.assertEqual(default_domain_name, p._plugin.project_domain_name) self.assertEqual(project_name, p._plugin.project_name) return p def test_default_domain_name_no_v3(self): self.stub_discovery(v3=False) project_name = uuid.uuid4().hex default_domain_name = uuid.uuid4().hex p = self.assertCreateV2(project_name=project_name, default_domain_name=default_domain_name) self.assertEqual(project_name, p._plugin.tenant_name) keystoneauth-2.4.1/keystoneauth1/tests/unit/k2k_fixtures.py000066400000000000000000000125161271245473000241720ustar00rootroot00000000000000# 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. UNSCOPED_TOKEN_HEADER = 'UNSCOPED_TOKEN' UNSCOPED_TOKEN = { "token": { "issued_at": "2014-06-09T09:48:59.643406Z", "extras": {}, "methods": ["token"], "expires_at": "2014-06-09T10:48:59.643375Z", "user": { "OS-FEDERATION": { "identity_provider": { "id": "testshib" }, "protocol": { "id": "saml2" }, "groups": [ {"id": "1764fa5cf69a49a4918131de5ce4af9a"} ] }, "id": "testhib%20user", "name": "testhib user" } } } SAML_ENCODING = "" TOKEN_SAML_RESPONSE = """ http://keystone.idp/v3/OS-FEDERATION/saml2/idp http://keystone.idp/v3/OS-FEDERATION/saml2/idp 0KH2CxdkfzU+6eiRhTC+mbObUKI= m2jh5gDvX/1k+4uKtbb08CHp2b9UWsLw ... admin urn:oasis:names:tc:SAML:2.0:ac:classes:Password http://keystone.idp/v3/OS-FEDERATION/saml2/idp admin admin admin """ TOKEN_BASED_SAML = ''.join([SAML_ENCODING, TOKEN_SAML_RESPONSE]) ECP_ENVELOPE = """ ss:mem:1ddfe8b0f58341a5a840d2e8717b0737 {0} """.format(TOKEN_SAML_RESPONSE) TOKEN_BASED_ECP = ''.join([SAML_ENCODING, ECP_ENVELOPE]) keystoneauth-2.4.1/keystoneauth1/tests/unit/keystoneauth_fixtures.py000066400000000000000000000045121271245473000262230ustar00rootroot00000000000000# 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 fixtures class HackingCode(fixtures.Fixture): """A fixture to house the various code examples. Examples contains various keystoneauth hacking style checks. """ oslo_namespace_imports = { 'code': """ import oslo.utils import oslo_utils import oslo.utils.encodeutils import oslo_utils.encodeutils from oslo import utils from oslo.utils import encodeutils from oslo_utils import encodeutils import oslo.serialization import oslo_serialization import oslo.serialization.jsonutils import oslo_serialization.jsonutils from oslo import serialization from oslo.serialization import jsonutils from oslo_serialization import jsonutils import oslo.config import oslo_config import oslo.config.cfg import oslo_config.cfg from oslo import config from oslo.config import cfg from oslo_config import cfg import oslo.i18n import oslo_i18n import oslo.i18n.log import oslo_i18n.log from oslo import i18n from oslo.i18n import log from oslo_i18n import log """, 'expected_errors': [ (1, 0, 'K333'), (3, 0, 'K333'), (5, 0, 'K333'), (6, 0, 'K333'), (9, 0, 'K333'), (11, 0, 'K333'), (13, 0, 'K333'), (14, 0, 'K333'), (17, 0, 'K333'), (19, 0, 'K333'), (21, 0, 'K333'), (22, 0, 'K333'), (25, 0, 'K333'), (27, 0, 'K333'), (29, 0, 'K333'), (30, 0, 'K333'), ], } keystoneauth-2.4.1/keystoneauth1/tests/unit/loading/000077500000000000000000000000001271245473000226105ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/loading/__init__.py000066400000000000000000000000001271245473000247070ustar00rootroot00000000000000keystoneauth-2.4.1/keystoneauth1/tests/unit/loading/test_cli.py000066400000000000000000000177171271245473000250050ustar00rootroot00000000000000# 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 argparse import uuid import fixtures import mock from keystoneauth1 import adapter from keystoneauth1 import loading from keystoneauth1.loading import cli from keystoneauth1.tests.unit.loading import utils TesterPlugin, TesterLoader = utils.create_plugin( opts=[ loading.Opt('test-opt', help='tester', deprecated=[loading.Opt('test-other')]) ] ) class CliTests(utils.TestCase): def setUp(self): super(CliTests, self).setUp() self.p = argparse.ArgumentParser() def env(self, name, value=None): if value is not None: # environment variables are always strings value = str(value) return self.useFixture(fixtures.EnvironmentVariable(name, value)) def test_creating_with_no_args(self): ret = loading.register_auth_argparse_arguments(self.p, []) self.assertIsNone(ret) self.assertIn('--os-auth-type', self.p.format_usage()) def test_load_with_nothing(self): loading.register_auth_argparse_arguments(self.p, []) opts = self.p.parse_args([]) self.assertIsNone(loading.load_auth_from_argparse_arguments(opts)) @utils.mock_plugin() def test_basic_params_added(self, m): name = uuid.uuid4().hex argv = ['--os-auth-plugin', name] ret = loading.register_auth_argparse_arguments(self.p, argv) self.assertIsInstance(ret, utils.MockLoader) for n in ('--os-a-int', '--os-a-bool', '--os-a-float'): self.assertIn(n, self.p.format_usage()) m.assert_called_once_with(name) @utils.mock_plugin() def test_param_loading(self, m): name = uuid.uuid4().hex argv = ['--os-auth-type', name, '--os-a-int', str(self.a_int), '--os-a-float', str(self.a_float), '--os-a-bool', str(self.a_bool)] klass = loading.register_auth_argparse_arguments(self.p, argv) self.assertIsInstance(klass, utils.MockLoader) opts = self.p.parse_args(argv) self.assertEqual(name, opts.os_auth_type) a = loading.load_auth_from_argparse_arguments(opts) self.assertTestVals(a) self.assertEqual(name, opts.os_auth_type) self.assertEqual(str(self.a_int), opts.os_a_int) self.assertEqual(str(self.a_float), opts.os_a_float) self.assertEqual(str(self.a_bool), opts.os_a_bool) @utils.mock_plugin() def test_default_options(self, m): name = uuid.uuid4().hex argv = ['--os-auth-type', name, '--os-a-float', str(self.a_float)] klass = loading.register_auth_argparse_arguments(self.p, argv) self.assertIsInstance(klass, utils.MockLoader) opts = self.p.parse_args(argv) self.assertEqual(name, opts.os_auth_type) a = loading.load_auth_from_argparse_arguments(opts) self.assertEqual(self.a_float, a['a_float']) self.assertEqual(3, a['a_int']) @utils.mock_plugin() def test_with_default_string_value(self, m): name = uuid.uuid4().hex klass = loading.register_auth_argparse_arguments(self.p, [], default=name) self.assertIsInstance(klass, utils.MockLoader) m.assert_called_once_with(name) @utils.mock_plugin() def test_overrides_default_string_value(self, m): name = uuid.uuid4().hex default = uuid.uuid4().hex argv = ['--os-auth-type', name] klass = loading.register_auth_argparse_arguments(self.p, argv, default=default) self.assertIsInstance(klass, utils.MockLoader) m.assert_called_once_with(name) @utils.mock_plugin() def test_with_default_type_value(self, m): default = utils.MockLoader() klass = loading.register_auth_argparse_arguments(self.p, [], default=default) self.assertIsInstance(klass, utils.MockLoader) self.assertEqual(0, m.call_count) @utils.mock_plugin() def test_overrides_default_type_value(self, m): # using this test plugin would fail if called because there # is no get_options() function class TestLoader(object): pass name = uuid.uuid4().hex argv = ['--os-auth-type', name] klass = loading.register_auth_argparse_arguments(self.p, argv, default=TestLoader) self.assertIsInstance(klass, utils.MockLoader) m.assert_called_once_with(name) @utils.mock_plugin() def test_env_overrides_default_opt(self, m): name = uuid.uuid4().hex val = uuid.uuid4().hex self.env('OS_A_STR', val) klass = loading.register_auth_argparse_arguments(self.p, [], default=name) self.assertIsInstance(klass, utils.MockLoader) opts = self.p.parse_args([]) a = loading.load_auth_from_argparse_arguments(opts) self.assertEqual(val, a['a_str']) def test_deprecated_cli_options(self): cli._register_plugin_argparse_arguments(self.p, TesterLoader()) val = uuid.uuid4().hex opts = self.p.parse_args(['--os-test-other', val]) self.assertEqual(val, opts.os_test_opt) def test_deprecated_multi_cli_options(self): cli._register_plugin_argparse_arguments(self.p, TesterLoader()) val1 = uuid.uuid4().hex val2 = uuid.uuid4().hex # argarse rules say that the last specified wins. opts = self.p.parse_args(['--os-test-other', val2, '--os-test-opt', val1]) self.assertEqual(val1, opts.os_test_opt) def test_deprecated_env_options(self): val = uuid.uuid4().hex with mock.patch.dict('os.environ', {'OS_TEST_OTHER': val}): cli._register_plugin_argparse_arguments(self.p, TesterLoader()) opts = self.p.parse_args([]) self.assertEqual(val, opts.os_test_opt) def test_deprecated_env_multi_options(self): val1 = uuid.uuid4().hex val2 = uuid.uuid4().hex with mock.patch.dict('os.environ', {'OS_TEST_OPT': val1, 'OS_TEST_OTHER': val2}): cli._register_plugin_argparse_arguments(self.p, TesterLoader()) opts = self.p.parse_args([]) self.assertEqual(val1, opts.os_test_opt) def test_adapter_service_type(self): argv = ['--os-service-type', 'compute'] adapter.Adapter.register_argparse_arguments(self.p, 'compute') opts = self.p.parse_args(argv) self.assertEqual('compute', opts.os_service_type) self.assertFalse(hasattr(opts, 'os_compute_service_type')) def test_adapter_service_type_per_service(self): argv = ['--os-compute-service-type', 'weirdness'] adapter.Adapter.register_argparse_arguments(self.p, 'compute') adapter.Adapter.register_service_argparse_arguments(self.p, 'compute') opts = self.p.parse_args(argv) self.assertEqual('compute', opts.os_service_type) self.assertEqual('weirdness', opts.os_compute_service_type) keystoneauth-2.4.1/keystoneauth1/tests/unit/loading/test_conf.py000066400000000000000000000202541271245473000251510ustar00rootroot00000000000000# 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 uuid import mock from oslo_config import cfg from oslo_config import fixture as config import stevedore from keystoneauth1 import exceptions from keystoneauth1 import loading from keystoneauth1.loading._plugins.identity import v2 from keystoneauth1.loading._plugins.identity import v3 from keystoneauth1.tests.unit.loading import utils class ConfTests(utils.TestCase): def setUp(self): super(ConfTests, self).setUp() self.conf_fixture = self.useFixture(config.Config()) # NOTE(jamielennox): we register the basic config options first because # we need them in place before we can stub them. We will need to run # the register again after we stub the auth section and auth plugin so # it can load the plugin specific options. loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) def test_loading_v2(self): section = uuid.uuid4().hex auth_url = uuid.uuid4().hex username = uuid.uuid4().hex password = uuid.uuid4().hex trust_id = uuid.uuid4().hex tenant_id = uuid.uuid4().hex self.conf_fixture.config(auth_section=section, group=self.GROUP) loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) opts = loading.get_auth_plugin_conf_options(v2.Password()) self.conf_fixture.register_opts(opts, group=section) self.conf_fixture.config(auth_type=self.V2PASS, auth_url=auth_url, username=username, password=password, trust_id=trust_id, tenant_id=tenant_id, group=section) a = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertEqual(auth_url, a.auth_url) self.assertEqual(username, a.username) self.assertEqual(password, a.password) self.assertEqual(trust_id, a.trust_id) self.assertEqual(tenant_id, a.tenant_id) def test_loading_v3(self): section = uuid.uuid4().hex auth_url = uuid.uuid4().hex, token = uuid.uuid4().hex trust_id = uuid.uuid4().hex project_id = uuid.uuid4().hex project_domain_name = uuid.uuid4().hex self.conf_fixture.config(auth_section=section, group=self.GROUP) loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) opts = loading.get_auth_plugin_conf_options(v3.Token()) self.conf_fixture.register_opts(opts, group=section) self.conf_fixture.config(auth_type=self.V3TOKEN, auth_url=auth_url, token=token, trust_id=trust_id, project_id=project_id, project_domain_name=project_domain_name, group=section) a = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertEqual(token, a.auth_methods[0].token) self.assertEqual(trust_id, a.trust_id) self.assertEqual(project_id, a.project_id) self.assertEqual(project_domain_name, a.project_domain_name) def test_loading_invalid_plugin(self): auth_type = uuid.uuid4().hex self.conf_fixture.config(auth_type=auth_type, group=self.GROUP) e = self.assertRaises(exceptions.NoMatchingPlugin, loading.load_auth_from_conf_options, self.conf_fixture.conf, self.GROUP) self.assertEqual(auth_type, e.name) def test_loading_with_no_data(self): l = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertIsNone(l) @mock.patch('stevedore.DriverManager') def test_other_params(self, m): m.return_value = utils.MockManager(utils.MockLoader()) driver_name = uuid.uuid4().hex opts = loading.get_auth_plugin_conf_options(utils.MockLoader()) self.conf_fixture.register_opts(opts, group=self.GROUP) self.conf_fixture.config(auth_type=driver_name, group=self.GROUP, **self.TEST_VALS) a = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertTestVals(a) m.assert_called_once_with(namespace=loading.PLUGIN_NAMESPACE, name=driver_name, invoke_on_load=True) @utils.mock_plugin() def test_same_section(self, m): opts = loading.get_auth_plugin_conf_options(utils.MockLoader()) self.conf_fixture.register_opts(opts, group=self.GROUP) loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) self.conf_fixture.config(auth_type=uuid.uuid4().hex, group=self.GROUP, **self.TEST_VALS) a = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertTestVals(a) @utils.mock_plugin() def test_diff_section(self, m): section = uuid.uuid4().hex self.conf_fixture.config(auth_section=section, group=self.GROUP) loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) opts = loading.get_auth_plugin_conf_options(utils.MockLoader()) self.conf_fixture.register_opts(opts, group=section) self.conf_fixture.config(group=section, auth_type=uuid.uuid4().hex, **self.TEST_VALS) a = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertTestVals(a) def test_plugins_are_all_opts(self): manager = stevedore.ExtensionManager(loading.PLUGIN_NAMESPACE, propagate_map_exceptions=True) def inner(driver): for p in driver.plugin().get_options(): self.assertIsInstance(p, loading.Opt) manager.map(inner) def test_get_common(self): opts = loading.get_auth_common_conf_options() for opt in opts: self.assertIsInstance(opt, cfg.Opt) self.assertEqual(2, len(opts)) def test_get_named(self): loaded_opts = loading.get_plugin_options('v2password') plugin_opts = v2.Password().get_options() loaded_names = set([o.name for o in loaded_opts]) plugin_names = set([o.name for o in plugin_opts]) self.assertEqual(plugin_names, loaded_names) def test_register_cfg(self): loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) def test_common_conf_options(self): opts = loading.get_auth_common_conf_options() self.assertEqual(2, len(opts)) auth_type = [o for o in opts if o.name == 'auth_type'][0] self.assertEqual(1, len(auth_type.deprecated_opts)) self.assertIsInstance(auth_type.deprecated_opts[0], cfg.DeprecatedOpt) keystoneauth-2.4.1/keystoneauth1/tests/unit/loading/test_generic.py000066400000000000000000000057221271245473000256430ustar00rootroot00000000000000# 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 uuid from keystoneauth1 import fixture from keystoneauth1 import identity from keystoneauth1.loading._plugins.identity import generic from keystoneauth1 import session from keystoneauth1.tests.unit.loading import utils class PasswordTests(utils.TestCase): def test_options(self): opts = [o.name for o in generic.Password().get_options()] allowed_opts = ['username', 'user-domain-id', 'user-domain-name', 'user-id', 'password', 'domain-id', 'domain-name', 'project-id', 'project-name', 'project-domain-id', 'project-domain-name', 'trust-id', 'auth-url', 'default-domain-id', 'default-domain-name', ] self.assertEqual(set(allowed_opts), set(opts)) self.assertEqual(len(allowed_opts), len(opts)) def test_loads_v3_with_user_domain(self): auth_url = 'http://keystone.test:5000' disc = fixture.DiscoveryList(href=auth_url) sess = session.Session() self.requests_mock.get(auth_url, json=disc) plugin = generic.Password().load_from_options( auth_url=auth_url, user_id=uuid.uuid4().hex, password=uuid.uuid4().hex, project_id=uuid.uuid4().hex, user_domain_id=uuid.uuid4().hex) inner_plugin = plugin._do_create_plugin(sess) self.assertIsInstance(inner_plugin, identity.V3Password) self.assertEqual(inner_plugin.auth_url, auth_url + '/v3') class TokenTests(utils.TestCase): def test_options(self): opts = [o.name for o in generic.Token().get_options()] allowed_opts = ['token', 'domain-id', 'domain-name', 'project-id', 'project-name', 'project-domain-id', 'project-domain-name', 'trust-id', 'auth-url', 'default-domain-id', 'default-domain-name', ] self.assertEqual(set(allowed_opts), set(opts)) self.assertEqual(len(allowed_opts), len(opts)) keystoneauth-2.4.1/keystoneauth1/tests/unit/loading/test_loading.py000066400000000000000000000063251271245473000256440ustar00rootroot00000000000000# 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 uuid from testtools import matchers from keystoneauth1 import exceptions from keystoneauth1 import loading from keystoneauth1.tests.unit.loading import utils class LoadingTests(utils.TestCase): def test_required_values(self): opts = [loading.Opt('a', required=False), loading.Opt('b', required=True)] Plugin, Loader = utils.create_plugin(opts=opts) l = Loader() v = uuid.uuid4().hex p1 = l.load_from_options(b=v) self.assertEqual(v, p1['b']) e = self.assertRaises(exceptions.MissingRequiredOptions, l.load_from_options, a=v) self.assertEqual(1, len(e.options)) for o in e.options: self.assertIsInstance(o, loading.Opt) self.assertEqual('b', e.options[0].name) def test_loaders(self): loaders = loading.get_available_plugin_loaders() self.assertThat(len(loaders), matchers.GreaterThan(0)) for l in loaders.values(): self.assertIsInstance(l, loading.BaseLoader) def test_loading_getter(self): called_opts = [] vals = {'a-int': 44, 'a-bool': False, 'a-float': 99.99, 'a-str': 'value'} val = uuid.uuid4().hex def _getter(opt): called_opts.append(opt.name) # return str because oslo.config should convert them back return str(vals[opt.name]) p = utils.MockLoader().load_from_options_getter(_getter, other=val) self.assertEqual(set(vals), set(called_opts)) for k, v in vals.items(): # replace - to _ because it's the dest used to create kwargs self.assertEqual(v, p[k.replace('-', '_')]) # check that additional kwargs get passed through self.assertEqual(val, p['other']) def test_loading_getter_with_kwargs(self): called_opts = set() vals = {'a-bool': False, 'a-float': 99.99} def _getter(opt): called_opts.add(opt.name) # return str because oslo.config should convert them back return str(vals[opt.name]) p = utils.MockLoader().load_from_options_getter(_getter, a_int=66, a_str='another') # only the options not passed by kwargs should get passed to getter self.assertEqual(set(('a-bool', 'a-float')), called_opts) self.assertEqual(False, p['a_bool']) self.assertEqual(99.99, p['a_float']) self.assertEqual('another', p['a_str']) self.assertEqual(66, p['a_int']) keystoneauth-2.4.1/keystoneauth1/tests/unit/loading/test_session.py000066400000000000000000000067641271245473000257210ustar00rootroot00000000000000# 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 argparse import uuid from oslo_config import cfg from oslo_config import fixture as config from testtools import matchers from keystoneauth1 import loading from keystoneauth1.tests.unit.loading import utils class ConfLoadingTests(utils.TestCase): GROUP = 'sessiongroup' def setUp(self): super(ConfLoadingTests, self).setUp() self.conf_fixture = self.useFixture(config.Config()) loading.register_session_conf_options(self.conf_fixture.conf, self.GROUP) def config(self, **kwargs): kwargs['group'] = self.GROUP self.conf_fixture.config(**kwargs) def get_session(self, **kwargs): return loading.load_session_from_conf_options(self.conf_fixture.conf, self.GROUP, **kwargs) def test_insecure_timeout(self): self.config(insecure=True, timeout=5) s = self.get_session() self.assertFalse(s.verify) self.assertEqual(5, s.timeout) def test_client_certs(self): cert = '/path/to/certfile' key = '/path/to/keyfile' self.config(certfile=cert, keyfile=key) s = self.get_session() self.assertTrue(s.verify) self.assertEqual((cert, key), s.cert) def test_cacert(self): cafile = '/path/to/cacert' self.config(cafile=cafile) s = self.get_session() self.assertEqual(cafile, s.verify) def test_deprecated(self): def new_deprecated(): return cfg.DeprecatedOpt(uuid.uuid4().hex, group=uuid.uuid4().hex) opt_names = ['cafile', 'certfile', 'keyfile', 'insecure', 'timeout'] depr = dict([(n, [new_deprecated()]) for n in opt_names]) opts = loading.get_session_conf_options(deprecated_opts=depr) self.assertThat(opt_names, matchers.HasLength(len(opts))) for opt in opts: self.assertIn(depr[opt.name][0], opt.deprecated_opts) class CliLoadingTests(utils.TestCase): def setUp(self): super(CliLoadingTests, self).setUp() self.parser = argparse.ArgumentParser() loading.register_session_argparse_arguments(self.parser) def get_session(self, val, **kwargs): args = self.parser.parse_args(val.split()) return loading.load_session_from_argparse_arguments(args, **kwargs) def test_insecure_timeout(self): s = self.get_session('--insecure --timeout 5.5') self.assertFalse(s.verify) self.assertEqual(5.5, s.timeout) def test_client_certs(self): cert = '/path/to/certfile' key = '/path/to/keyfile' s = self.get_session('--os-cert %s --os-key %s' % (cert, key)) self.assertTrue(s.verify) self.assertEqual((cert, key), s.cert) def test_cacert(self): cacert = '/path/to/cacert' s = self.get_session('--os-cacert %s' % cacert) self.assertEqual(cacert, s.verify) keystoneauth-2.4.1/keystoneauth1/tests/unit/loading/test_v3.py000066400000000000000000000046011271245473000245520ustar00rootroot00000000000000# 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 uuid from keystoneauth1 import exceptions from keystoneauth1 import loading from keystoneauth1.tests.unit.loading import utils class V3PasswordTests(utils.TestCase): def setUp(self): super(V3PasswordTests, self).setUp() self.auth_url = uuid.uuid4().hex def create(self, **kwargs): kwargs.setdefault('auth_url', self.auth_url) loader = loading.get_plugin_loader('v3password') return loader.load_from_options(**kwargs) def test_basic(self): username = uuid.uuid4().hex user_domain_id = uuid.uuid4().hex password = uuid.uuid4().hex project_name = uuid.uuid4().hex project_domain_id = uuid.uuid4().hex p = self.create(username=username, user_domain_id=user_domain_id, project_name=project_name, project_domain_id=project_domain_id, password=password) pw_method = p.auth_methods[0] self.assertEqual(username, pw_method.username) self.assertEqual(user_domain_id, pw_method.user_domain_id) self.assertEqual(password, pw_method.password) self.assertEqual(project_name, p.project_name) self.assertEqual(project_domain_id, p.project_domain_id) def test_without_user_domain(self): self.assertRaises(exceptions.OptionError, self.create, username=uuid.uuid4().hex, password=uuid.uuid4().hex) def test_without_project_domain(self): self.assertRaises(exceptions.OptionError, self.create, username=uuid.uuid4().hex, password=uuid.uuid4().hex, user_domain_id=uuid.uuid4().hex, project_name=uuid.uuid4().hex) keystoneauth-2.4.1/keystoneauth1/tests/unit/loading/utils.py000066400000000000000000000055631271245473000243330ustar00rootroot00000000000000# 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 functools import uuid import mock import six from keystoneauth1 import loading from keystoneauth1.loading import base from keystoneauth1 import plugin from keystoneauth1.tests.unit import utils class TestCase(utils.TestCase): GROUP = 'auth' V2PASS = 'v2password' V3TOKEN = 'v3token' a_int = 88 a_float = 88.8 a_bool = False TEST_VALS = {'a_int': a_int, 'a_float': a_float, 'a_bool': a_bool} def assertTestVals(self, plugin, vals=TEST_VALS): for k, v in six.iteritems(vals): self.assertEqual(v, plugin[k]) def create_plugin(opts=[], token=None, endpoint=None): class Plugin(plugin.BaseAuthPlugin): def __init__(self, **kwargs): self._data = kwargs def __getitem__(self, key): return self._data[key] def get_token(self, *args, **kwargs): return token def get_endpoint(self, *args, **kwargs): return endpoint class Loader(loading.BaseLoader): @property def plugin_class(self): return Plugin def get_options(self): return opts return Plugin, Loader class BoolType(object): def __eq__(self, other): # hack around oslo.config type comparison return type(self) == type(other) def __call__(self, value): return str(value).lower() in ('1', 'true', 't', 'yes', 'y') INT_DESC = 'test int' FLOAT_DESC = 'test float' BOOL_DESC = 'test bool' STR_DESC = 'test str' STR_DEFAULT = uuid.uuid4().hex MockPlugin, MockLoader = create_plugin( endpoint='http://test', token='aToken', opts=[ loading.Opt('a-int', default=3, type=int, help=INT_DESC), loading.Opt('a-bool', type=BoolType(), help=BOOL_DESC), loading.Opt('a-float', type=float, help=FLOAT_DESC), loading.Opt('a-str', help=STR_DESC, default=STR_DEFAULT), ] ) class MockManager(object): def __init__(self, driver): self.driver = driver def mock_plugin(loader=MockLoader): def _wrapper(f): @functools.wraps(f) def inner(*args, **kwargs): with mock.patch.object(base, 'get_plugin_loader') as m: m.return_value = loader() args = list(args) + [m] return f(*args, **kwargs) return inner return _wrapper keystoneauth-2.4.1/keystoneauth1/tests/unit/matchers.py000066400000000000000000000061361271245473000233610ustar00rootroot00000000000000# 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. from lxml import etree from testtools import matchers class XMLEquals(object): """Parses two XML documents from strings and compares the results. """ def __init__(self, expected): self.expected = expected def __str__(self): return "%s(%r)" % (self.__class__.__name__, self.expected) def match(self, other): def xml_element_equals(expected_doc, observed_doc): """Tests whether two XML documents are equivalent. This is a recursive algorithm that operates on each element in the hierarchy. Siblings are sorted before being checked to account for two semantically equivalent documents where siblings appear in different document order. The sorting algorithm is a little weak in that it could fail for documents where siblings at a given level are the same, but have different children. """ if expected_doc.tag != observed_doc.tag: return False if expected_doc.attrib != observed_doc.attrib: return False def _sorted_children(doc): return sorted(doc.getchildren(), key=lambda el: el.tag) expected_children = _sorted_children(expected_doc) observed_children = _sorted_children(observed_doc) if len(expected_children) != len(observed_children): return False for expected_el, observed_el in zip(expected_children, observed_children): if not xml_element_equals(expected_el, observed_el): return False return True parser = etree.XMLParser(remove_blank_text=True) expected_doc = etree.fromstring(self.expected.strip(), parser) observed_doc = etree.fromstring(other.strip(), parser) if xml_element_equals(expected_doc, observed_doc): return return XMLMismatch(self.expected, other) class XMLMismatch(matchers.Mismatch): def __init__(self, expected, other): self.expected = expected self.other = other def describe(self): def pretty_xml(xml): parser = etree.XMLParser(remove_blank_text=True) doc = etree.fromstring(xml.strip(), parser) return (etree.tostring(doc, encoding='utf-8', pretty_print=True) .decode('utf-8')) return 'expected =\n%s\nactual =\n%s' % ( pretty_xml(self.expected), pretty_xml(self.other)) keystoneauth-2.4.1/keystoneauth1/tests/unit/oidc_fixtures.py000066400000000000000000000032321271245473000244140ustar00rootroot00000000000000# 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. UNSCOPED_TOKEN = { "token": { "issued_at": "2014-06-09T09:48:59.643406Z", "extras": {}, "methods": ["oidc"], "expires_at": "2014-06-09T10:48:59.643375Z", "user": { "OS-FEDERATION": { "identity_provider": { "id": "bluepages" }, "protocol": { "id": "oidc" }, "groups": [ {"id": "1764fa5cf69a49a4918131de5ce4af9a"} ] }, "id": "oidc_user%40example.com", "name": "oidc_user@example.com" } } } ACCESS_TOKEN_VIA_PASSWORD_RESP = { "access_token": "z5H1ITZLlJVDHQXqJun", "token_type": "bearer", "expires_in": 3599, "scope": "profile", "refresh_token": "DCERsh83IAhu9bhavrp" } ACCESS_TOKEN_VIA_AUTH_GRANT_RESP = { "access_token": "ya29.jgGIjfVrBPWLStWSU3eh8ioE6hG06QQ", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "1/ySXNO9XISBMIgOrJDtdun6zK6XiATCKT", "id_token": "eyJhbGciOiJSUzI1Ni8hOYHuZT8dt_yynmJVhcU" } keystoneauth-2.4.1/keystoneauth1/tests/unit/test_betamax_fixture.py000066400000000000000000000042211271245473000257720ustar00rootroot00000000000000# 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 betamax import exceptions from keystoneauth1.fixture import keystoneauth_betamax from keystoneauth1.fixture import v2 as v2Fixtures from keystoneauth1.identity import v2 from keystoneauth1 import session class TestBetamaxFixture(testtools.TestCase): TEST_USERNAME = 'test_user_name' TEST_PASSWORD = 'test_password' TEST_TENANT_NAME = 'test_tenant_name' TEST_AUTH_URL = 'http://keystonauth.betamax_test/v2.0/' V2_TOKEN = v2Fixtures.Token(tenant_name=TEST_TENANT_NAME, user_name=TEST_USERNAME) def setUp(self): super(TestBetamaxFixture, self).setUp() self.ksa_betamax_fixture = self.useFixture( keystoneauth_betamax.BetamaxFixture( cassette_name='ksa_betamax_test_cassette', cassette_library_dir='keystoneauth1/tests/unit/data/', record=False)) def _replay_cassette(self): plugin = v2.Password( auth_url=self.TEST_AUTH_URL, password=self.TEST_PASSWORD, username=self.TEST_USERNAME, tenant_name=self.TEST_TENANT_NAME) s = session.Session() s.get_token(auth=plugin) def test_keystoneauth_betamax_fixture(self): self._replay_cassette() def test_replay_of_bad_url_fails(self): plugin = v2.Password( auth_url='http://invalid-auth-url/v2.0/', password=self.TEST_PASSWORD, username=self.TEST_USERNAME, tenant_name=self.TEST_TENANT_NAME) s = session.Session() self.assertRaises(exceptions.BetamaxError, s.get_token, auth=plugin) keystoneauth-2.4.1/keystoneauth1/tests/unit/test_discovery.py000066400000000000000000000171011271245473000246130ustar00rootroot00000000000000# 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 re import six from keystoneauth1 import discover from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils BASE_HOST = 'http://keystone.example.com' BASE_URL = "%s:5000/" % BASE_HOST UPDATED = '2013-03-06T00:00:00Z' TEST_SERVICE_CATALOG = [{ "endpoints": [{ "adminURL": "%s:8774/v1.0" % BASE_HOST, "region": "RegionOne", "internalURL": "%s://127.0.0.1:8774/v1.0" % BASE_HOST, "publicURL": "%s:8774/v1.0/" % BASE_HOST }], "type": "nova_compat", "name": "nova_compat" }, { "endpoints": [{ "adminURL": "http://nova/novapi/admin", "region": "RegionOne", "internalURL": "http://nova/novapi/internal", "publicURL": "http://nova/novapi/public" }], "type": "compute", "name": "nova" }, { "endpoints": [{ "adminURL": "http://glance/glanceapi/admin", "region": "RegionOne", "internalURL": "http://glance/glanceapi/internal", "publicURL": "http://glance/glanceapi/public" }], "type": "image", "name": "glance" }, { "endpoints": [{ "adminURL": "%s:35357/v2.0" % BASE_HOST, "region": "RegionOne", "internalURL": "%s:5000/v2.0" % BASE_HOST, "publicURL": "%s:5000/v2.0" % BASE_HOST }], "type": "identity", "name": "keystone" }, { "endpoints": [{ "adminURL": "http://swift/swiftapi/admin", "region": "RegionOne", "internalURL": "http://swift/swiftapi/internal", "publicURL": "http://swift/swiftapi/public" }], "type": "object-store", "name": "swift" }] V2_URL = "%sv2.0" % BASE_URL V2_VERSION = fixture.V2Discovery(V2_URL) V2_VERSION.updated_str = UPDATED V2_AUTH_RESPONSE = json.dumps({ "access": { "token": { "expires": "2020-01-01T00:00:10.000123Z", "id": 'fakeToken', "tenant": { "id": '1' }, }, "user": { "id": 'test' }, "serviceCatalog": TEST_SERVICE_CATALOG, }, }) V3_URL = "%sv3" % BASE_URL V3_VERSION = fixture.V3Discovery(V3_URL) V3_MEDIA_TYPES = V3_VERSION.media_types V3_VERSION.updated_str = UPDATED V3_TOKEN = six.u('3e2813b7ba0b4006840c3825860b86ed'), V3_AUTH_RESPONSE = json.dumps({ "token": { "methods": [ "token", "password" ], "expires_at": "2020-01-01T00:00:10.000123Z", "project": { "domain": { "id": '1', "name": 'test-domain' }, "id": '1', "name": 'test-project' }, "user": { "domain": { "id": '1', "name": 'test-domain' }, "id": '1', "name": 'test-user' }, "issued_at": "2013-05-29T16:55:21.468960Z", }, }) CINDER_EXAMPLES = { "versions": [ { "status": "CURRENT", "updated": "2012-01-04T11:33:21Z", "id": "v1.0", "links": [ { "href": "%sv1/" % BASE_URL, "rel": "self" } ] }, { "status": "CURRENT", "updated": "2012-11-21T11:33:21Z", "id": "v2.0", "links": [ { "href": "%sv2/" % BASE_URL, "rel": "self" } ] } ] } GLANCE_EXAMPLES = { "versions": [ { "status": "CURRENT", "id": "v2.2", "links": [ { "href": "%sv2/" % BASE_URL, "rel": "self" } ] }, { "status": "SUPPORTED", "id": "v2.1", "links": [ { "href": "%sv2/" % BASE_URL, "rel": "self" } ] }, { "status": "SUPPORTED", "id": "v2.0", "links": [ { "href": "%sv2/" % BASE_URL, "rel": "self" } ] }, { "status": "CURRENT", "id": "v1.1", "links": [ { "href": "%sv1/" % BASE_URL, "rel": "self" } ] }, { "status": "SUPPORTED", "id": "v1.0", "links": [ { "href": "%sv1/" % BASE_URL, "rel": "self" } ] } ] } def _create_version_list(versions): return json.dumps({'versions': {'values': versions}}) def _create_single_version(version): return json.dumps({'version': version}) V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION]) V2_VERSION_LIST = _create_version_list([V2_VERSION]) V3_VERSION_ENTRY = _create_single_version(V3_VERSION) V2_VERSION_ENTRY = _create_single_version(V2_VERSION) class CatalogHackTests(utils.TestCase): TEST_URL = 'http://keystone.server:5000/v2.0' OTHER_URL = 'http://other.server:5000/path' IDENTITY = 'identity' BASE_URL = 'http://keystone.server:5000/' V2_URL = BASE_URL + 'v2.0' V3_URL = BASE_URL + 'v3' def setUp(self): super(CatalogHackTests, self).setUp() self.hacks = discover._VersionHacks() self.hacks.add_discover_hack(self.IDENTITY, re.compile('/v2.0/?$'), '/') def test_version_hacks(self): self.assertEqual(self.BASE_URL, self.hacks.get_discover_hack(self.IDENTITY, self.V2_URL)) self.assertEqual(self.BASE_URL, self.hacks.get_discover_hack(self.IDENTITY, self.V2_URL + '/')) self.assertEqual(self.OTHER_URL, self.hacks.get_discover_hack(self.IDENTITY, self.OTHER_URL)) def test_ignored_non_service_type(self): self.assertEqual(self.V2_URL, self.hacks.get_discover_hack('other', self.V2_URL)) class DiscoverUtils(utils.TestCase): def test_version_number(self): def assertVersion(inp, out): self.assertEqual(out, discover.normalize_version_number(inp)) def versionRaises(inp): self.assertRaises(TypeError, discover.normalize_version_number, inp) assertVersion('v1.2', (1, 2)) assertVersion('v11', (11, 0)) assertVersion('1.2', (1, 2)) assertVersion('1.5.1', (1, 5, 1)) assertVersion('1', (1, 0)) assertVersion(1, (1, 0)) assertVersion(5.2, (5, 2)) assertVersion((6, 1), (6, 1)) assertVersion([1, 4], (1, 4)) versionRaises('hello') versionRaises('1.a') versionRaises('vacuum') keystoneauth-2.4.1/keystoneauth1/tests/unit/test_fixtures.py000066400000000000000000000264001271245473000244570ustar00rootroot00000000000000# 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 uuid import six from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils class V2TokenTests(utils.TestCase): def test_unscoped(self): token_id = uuid.uuid4().hex user_id = uuid.uuid4().hex user_name = uuid.uuid4().hex token = fixture.V2Token(token_id=token_id, user_id=user_id, user_name=user_name) self.assertEqual(token_id, token.token_id) self.assertEqual(token_id, token['access']['token']['id']) self.assertEqual(user_id, token.user_id) self.assertEqual(user_id, token['access']['user']['id']) self.assertEqual(user_name, token.user_name) self.assertEqual(user_name, token['access']['user']['name']) self.assertIsNone(token.trust_id) def test_tenant_scoped(self): tenant_id = uuid.uuid4().hex tenant_name = uuid.uuid4().hex token = fixture.V2Token(tenant_id=tenant_id, tenant_name=tenant_name) self.assertEqual(tenant_id, token.tenant_id) self.assertEqual(tenant_id, token['access']['token']['tenant']['id']) self.assertEqual(tenant_name, token.tenant_name) tn = token['access']['token']['tenant']['name'] self.assertEqual(tenant_name, tn) self.assertIsNone(token.trust_id) def test_trust_scoped(self): trust_id = uuid.uuid4().hex trustee_user_id = uuid.uuid4().hex token = fixture.V2Token(trust_id=trust_id, trustee_user_id=trustee_user_id) trust = token['access']['trust'] self.assertEqual(trust_id, token.trust_id) self.assertEqual(trust_id, trust['id']) self.assertEqual(trustee_user_id, token.trustee_user_id) self.assertEqual(trustee_user_id, trust['trustee_user_id']) def test_roles(self): role_id1 = uuid.uuid4().hex role_name1 = uuid.uuid4().hex role_id2 = uuid.uuid4().hex role_name2 = uuid.uuid4().hex token = fixture.V2Token() token.add_role(id=role_id1, name=role_name1) token.add_role(id=role_id2, name=role_name2) role_names = token['access']['user']['roles'] role_ids = token['access']['metadata']['roles'] self.assertEqual(set([role_id1, role_id2]), set(role_ids)) for r in (role_name1, role_name2): self.assertIn({'name': r}, role_names) def test_services(self): service_type = uuid.uuid4().hex service_name = uuid.uuid4().hex endpoint_id = uuid.uuid4().hex region = uuid.uuid4().hex public = uuid.uuid4().hex admin = uuid.uuid4().hex internal = uuid.uuid4().hex token = fixture.V2Token() svc = token.add_service(type=service_type, name=service_name) svc.add_endpoint(public=public, admin=admin, internal=internal, region=region, id=endpoint_id) self.assertEqual(1, len(token['access']['serviceCatalog'])) service = token['access']['serviceCatalog'][0]['endpoints'][0] self.assertEqual(public, service['publicURL']) self.assertEqual(internal, service['internalURL']) self.assertEqual(admin, service['adminURL']) self.assertEqual(region, service['region']) self.assertEqual(endpoint_id, service['id']) def test_token_bind(self): name1 = uuid.uuid4().hex data1 = uuid.uuid4().hex name2 = uuid.uuid4().hex data2 = {uuid.uuid4().hex: uuid.uuid4().hex} token = fixture.V2Token() token.set_bind(name1, data1) token.set_bind(name2, data2) self.assertEqual({name1: data1, name2: data2}, token['access']['token']['bind']) class V3TokenTests(utils.TestCase): def test_unscoped(self): user_id = uuid.uuid4().hex user_name = uuid.uuid4().hex user_domain_id = uuid.uuid4().hex user_domain_name = uuid.uuid4().hex token = fixture.V3Token(user_id=user_id, user_name=user_name, user_domain_id=user_domain_id, user_domain_name=user_domain_name) self.assertEqual(user_id, token.user_id) self.assertEqual(user_id, token['token']['user']['id']) self.assertEqual(user_name, token.user_name) self.assertEqual(user_name, token['token']['user']['name']) user_domain = token['token']['user']['domain'] self.assertEqual(user_domain_id, token.user_domain_id) self.assertEqual(user_domain_id, user_domain['id']) self.assertEqual(user_domain_name, token.user_domain_name) self.assertEqual(user_domain_name, user_domain['name']) def test_project_scoped(self): project_id = uuid.uuid4().hex project_name = uuid.uuid4().hex project_domain_id = uuid.uuid4().hex project_domain_name = uuid.uuid4().hex token = fixture.V3Token(project_id=project_id, project_name=project_name, project_domain_id=project_domain_id, project_domain_name=project_domain_name) self.assertEqual(project_id, token.project_id) self.assertEqual(project_id, token['token']['project']['id']) self.assertEqual(project_name, token.project_name) self.assertEqual(project_name, token['token']['project']['name']) project_domain = token['token']['project']['domain'] self.assertEqual(project_domain_id, token.project_domain_id) self.assertEqual(project_domain_id, project_domain['id']) self.assertEqual(project_domain_name, token.project_domain_name) self.assertEqual(project_domain_name, project_domain['name']) def test_domain_scoped(self): domain_id = uuid.uuid4().hex domain_name = uuid.uuid4().hex token = fixture.V3Token(domain_id=domain_id, domain_name=domain_name) self.assertEqual(domain_id, token.domain_id) self.assertEqual(domain_id, token['token']['domain']['id']) self.assertEqual(domain_name, token.domain_name) self.assertEqual(domain_name, token['token']['domain']['name']) def test_roles(self): role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} role2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} token = fixture.V3Token() token.add_role(**role1) token.add_role(**role2) self.assertEqual(2, len(token['token']['roles'])) self.assertIn(role1, token['token']['roles']) self.assertIn(role2, token['token']['roles']) def test_trust_scoped(self): trust_id = uuid.uuid4().hex trustee_user_id = uuid.uuid4().hex trustor_user_id = uuid.uuid4().hex impersonation = True token = fixture.V3Token(trust_id=trust_id, trustee_user_id=trustee_user_id, trustor_user_id=trustor_user_id, trust_impersonation=impersonation) trust = token['token']['OS-TRUST:trust'] self.assertEqual(trust_id, token.trust_id) self.assertEqual(trust_id, trust['id']) self.assertEqual(trustee_user_id, token.trustee_user_id) self.assertEqual(trustee_user_id, trust['trustee_user']['id']) self.assertEqual(trustor_user_id, token.trustor_user_id) self.assertEqual(trustor_user_id, trust['trustor_user']['id']) self.assertEqual(impersonation, token.trust_impersonation) self.assertEqual(impersonation, trust['impersonation']) def test_oauth_scoped(self): access_id = uuid.uuid4().hex consumer_id = uuid.uuid4().hex token = fixture.V3Token(oauth_access_token_id=access_id, oauth_consumer_id=consumer_id) oauth = token['token']['OS-OAUTH1'] self.assertEqual(access_id, token.oauth_access_token_id) self.assertEqual(access_id, oauth['access_token_id']) self.assertEqual(consumer_id, token.oauth_consumer_id) self.assertEqual(consumer_id, oauth['consumer_id']) def test_catalog(self): service_type = uuid.uuid4().hex service_name = uuid.uuid4().hex service_id = uuid.uuid4().hex region = uuid.uuid4().hex endpoints = {'public': uuid.uuid4().hex, 'internal': uuid.uuid4().hex, 'admin': uuid.uuid4().hex} token = fixture.V3Token() svc = token.add_service(type=service_type, name=service_name, id=service_id) svc.add_standard_endpoints(region=region, **endpoints) self.assertEqual(1, len(token['token']['catalog'])) service = token['token']['catalog'][0] self.assertEqual(3, len(service['endpoints'])) self.assertEqual(service_name, service['name']) self.assertEqual(service_type, service['type']) self.assertEqual(service_id, service['id']) for endpoint in service['endpoints']: # assert an id exists for each endpoint, remove it to make testing # the endpoint content below easier. self.assertTrue(endpoint.pop('id')) for interface, url in six.iteritems(endpoints): endpoint = {'interface': interface, 'url': url, 'region': region, 'region_id': region} self.assertIn(endpoint, service['endpoints']) def test_empty_default_service_providers(self): token = fixture.V3Token() self.assertIsNone(token.service_providers) def test_service_providers(self): def new_sp(): return { 'id': uuid.uuid4().hex, 'sp_url': uuid.uuid4().hex, 'auth_url': uuid.uuid4().hex } ref_service_providers = [new_sp(), new_sp()] token = fixture.V3Token() for sp in ref_service_providers: token.add_service_provider(sp['id'], sp['auth_url'], sp['sp_url']) self.assertEqual(ref_service_providers, token.service_providers) self.assertEqual(ref_service_providers, token['token']['service_providers']) def test_token_bind(self): name1 = uuid.uuid4().hex data1 = uuid.uuid4().hex name2 = uuid.uuid4().hex data2 = {uuid.uuid4().hex: uuid.uuid4().hex} token = fixture.V3Token() token.set_bind(name1, data1) token.set_bind(name2, data2) self.assertEqual({name1: data1, name2: data2}, token['token']['bind']) keystoneauth-2.4.1/keystoneauth1/tests/unit/test_hacking_checks.py000066400000000000000000000033201271245473000255260ustar00rootroot00000000000000# 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 textwrap import mock import pep8 import testtools from keystoneauth1.hacking import checks from keystoneauth1.tests.unit import keystoneauth_fixtures class TestCheckOsloNamespaceImports(testtools.TestCase): # We are patching pep8 so that only the check under test is actually # installed. @mock.patch('pep8._checks', {'physical_line': {}, 'logical_line': {}, 'tree': {}}) def run_check(self, code): pep8.register_check(checks.check_oslo_namespace_imports) lines = textwrap.dedent(code).strip().splitlines(True) checker = pep8.Checker(lines=lines) checker.check_all() checker.report._deferred_print.sort() return checker.report._deferred_print def assert_has_errors(self, code, expected_errors=None): actual_errors = [e[:3] for e in self.run_check(code)] self.assertEqual(expected_errors or [], actual_errors) def test(self): code_ex = self.useFixture(keystoneauth_fixtures.HackingCode()) code = code_ex.oslo_namespace_imports['code'] errors = code_ex.oslo_namespace_imports['expected_errors'] self.assert_has_errors(code, expected_errors=errors) keystoneauth-2.4.1/keystoneauth1/tests/unit/test_matchers.py000066400000000000000000000035561271245473000244230ustar00rootroot00000000000000# 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 testtools from testtools.tests.matchers import helpers from keystoneauth1.tests.unit import matchers class TestXMLEquals(testtools.TestCase, helpers.TestMatchersInterface): matches_xml = b""" """ equivalent_xml = b""" """ mismatches_xml = b""" """ mismatches_description = """expected = actual = """ matches_matcher = matchers.XMLEquals(matches_xml) matches_matches = [matches_xml, equivalent_xml] matches_mismatches = [mismatches_xml] describe_examples = [ (mismatches_description, mismatches_xml, matches_matcher), ] str_examples = [('XMLEquals(%r)' % matches_xml, matches_matcher)] keystoneauth-2.4.1/keystoneauth1/tests/unit/test_session.py000066400000000000000000001132051271245473000242710ustar00rootroot00000000000000# 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 itertools import json import logging import uuid import mock import requests import six from testtools import matchers from keystoneauth1 import adapter from keystoneauth1 import exceptions from keystoneauth1 import plugin from keystoneauth1 import session as client_session from keystoneauth1.tests.unit import utils from keystoneauth1 import token_endpoint class SessionTests(utils.TestCase): TEST_URL = 'http://127.0.0.1:5000/' def test_get(self): session = client_session.Session() self.stub_url('GET', text='response') resp = session.get(self.TEST_URL) self.assertEqual('GET', self.requests_mock.last_request.method) self.assertEqual(resp.text, 'response') self.assertTrue(resp.ok) def test_post(self): session = client_session.Session() self.stub_url('POST', text='response') resp = session.post(self.TEST_URL, json={'hello': 'world'}) self.assertEqual('POST', self.requests_mock.last_request.method) self.assertEqual(resp.text, 'response') self.assertTrue(resp.ok) self.assertRequestBodyIs(json={'hello': 'world'}) def test_head(self): session = client_session.Session() self.stub_url('HEAD') resp = session.head(self.TEST_URL) self.assertEqual('HEAD', self.requests_mock.last_request.method) self.assertTrue(resp.ok) self.assertRequestBodyIs('') def test_put(self): session = client_session.Session() self.stub_url('PUT', text='response') resp = session.put(self.TEST_URL, json={'hello': 'world'}) self.assertEqual('PUT', self.requests_mock.last_request.method) self.assertEqual(resp.text, 'response') self.assertTrue(resp.ok) self.assertRequestBodyIs(json={'hello': 'world'}) def test_delete(self): session = client_session.Session() self.stub_url('DELETE', text='response') resp = session.delete(self.TEST_URL) self.assertEqual('DELETE', self.requests_mock.last_request.method) self.assertTrue(resp.ok) self.assertEqual(resp.text, 'response') def test_patch(self): session = client_session.Session() self.stub_url('PATCH', text='response') resp = session.patch(self.TEST_URL, json={'hello': 'world'}) self.assertEqual('PATCH', self.requests_mock.last_request.method) self.assertTrue(resp.ok) self.assertEqual(resp.text, 'response') self.assertRequestBodyIs(json={'hello': 'world'}) def test_user_agent(self): custom_agent = 'custom-agent/1.0' session = client_session.Session(user_agent=custom_agent) self.stub_url('GET', text='response') resp = session.get(self.TEST_URL) self.assertTrue(resp.ok) self.assertRequestHeaderEqual( 'User-Agent', '%s %s' % (custom_agent, client_session.DEFAULT_USER_AGENT)) resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}) self.assertTrue(resp.ok) self.assertRequestHeaderEqual('User-Agent', 'new-agent') resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}, user_agent='overrides-agent') self.assertTrue(resp.ok) self.assertRequestHeaderEqual('User-Agent', 'overrides-agent') def test_http_session_opts(self): session = client_session.Session(cert='cert.pem', timeout=5, verify='certs') FAKE_RESP = utils.TestResponse({'status_code': 200, 'text': 'resp'}) RESP = mock.Mock(return_value=FAKE_RESP) with mock.patch.object(session.session, 'request', RESP) as mocked: session.post(self.TEST_URL, data='value') mock_args, mock_kwargs = mocked.call_args self.assertEqual(mock_args[0], 'POST') self.assertEqual(mock_args[1], self.TEST_URL) self.assertEqual(mock_kwargs['data'], 'value') self.assertEqual(mock_kwargs['cert'], 'cert.pem') self.assertEqual(mock_kwargs['verify'], 'certs') self.assertEqual(mock_kwargs['timeout'], 5) def test_not_found(self): session = client_session.Session() self.stub_url('GET', status_code=404) self.assertRaises(exceptions.NotFound, session.get, self.TEST_URL) def test_server_error(self): session = client_session.Session() self.stub_url('GET', status_code=500) self.assertRaises(exceptions.InternalServerError, session.get, self.TEST_URL) def test_session_debug_output(self): """Test request and response headers in debug logs in order to redact secure headers while debug is true. """ session = client_session.Session(verify=False) headers = {'HEADERA': 'HEADERVALB'} security_headers = {'Authorization': uuid.uuid4().hex, 'X-Auth-Token': uuid.uuid4().hex, 'X-Subject-Token': uuid.uuid4().hex, } body = 'BODYRESPONSE' data = 'BODYDATA' all_headers = dict( itertools.chain(headers.items(), security_headers.items())) self.stub_url('POST', text=body, headers=all_headers) resp = session.post(self.TEST_URL, headers=all_headers, data=data) self.assertEqual(resp.status_code, 200) self.assertIn('curl', self.logger.output) self.assertIn('POST', self.logger.output) self.assertIn('--insecure', self.logger.output) self.assertIn(body, self.logger.output) self.assertIn("'%s'" % data, self.logger.output) for k, v in six.iteritems(headers): self.assertIn(k, self.logger.output) self.assertIn(v, self.logger.output) # Assert that response headers contains actual values and # only debug logs has been masked for k, v in six.iteritems(security_headers): self.assertIn('%s: {SHA1}' % k, self.logger.output) self.assertEqual(v, resp.headers[k]) self.assertNotIn(v, self.logger.output) def test_logs_failed_output(self): """Test that output is logged even for failed requests""" session = client_session.Session() body = uuid.uuid4().hex self.stub_url('GET', text=body, status_code=400) resp = session.get(self.TEST_URL, raise_exc=False) self.assertEqual(resp.status_code, 400) self.assertIn(body, self.logger.output) def test_logging_cacerts(self): path_to_certs = '/path/to/certs' session = client_session.Session(verify=path_to_certs) self.stub_url('GET', text='text') session.get(self.TEST_URL) self.assertIn('--cacert', self.logger.output) self.assertIn(path_to_certs, self.logger.output) def test_connect_retries(self): self.stub_url('GET', exc=requests.exceptions.Timeout()) session = client_session.Session() retries = 3 with mock.patch('time.sleep') as m: self.assertRaises(exceptions.ConnectTimeout, session.get, self.TEST_URL, connect_retries=retries) self.assertEqual(retries, m.call_count) # 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0 m.assert_called_with(2.0) # we count retries so there will be one initial request + 3 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_uses_tcp_keepalive_by_default(self): session = client_session.Session() requests_session = session.session self.assertIsInstance(requests_session.adapters['http://'], client_session.TCPKeepAliveAdapter) self.assertIsInstance(requests_session.adapters['https://'], client_session.TCPKeepAliveAdapter) def test_does_not_set_tcp_keepalive_on_custom_sessions(self): mock_session = mock.Mock() client_session.Session(session=mock_session) self.assertFalse(mock_session.mount.called) def test_ssl_error_message(self): error = uuid.uuid4().hex self.stub_url('GET', exc=requests.exceptions.SSLError(error)) session = client_session.Session() # The exception should contain the URL and details about the SSL error msg = 'SSL exception connecting to %(url)s: %(error)s' % { 'url': self.TEST_URL, 'error': error} self.assertRaisesRegex(exceptions.SSLError, msg, session.get, self.TEST_URL) class RedirectTests(utils.TestCase): REDIRECT_CHAIN = ['http://myhost:3445/', 'http://anotherhost:6555/', 'http://thirdhost/', 'http://finaldestination:55/'] DEFAULT_REDIRECT_BODY = 'Redirect' DEFAULT_RESP_BODY = 'Found' def setup_redirects(self, method='GET', status_code=305, redirect_kwargs={}, final_kwargs={}): redirect_kwargs.setdefault('text', self.DEFAULT_REDIRECT_BODY) for s, d in zip(self.REDIRECT_CHAIN, self.REDIRECT_CHAIN[1:]): self.requests_mock.register_uri(method, s, status_code=status_code, headers={'Location': d}, **redirect_kwargs) final_kwargs.setdefault('status_code', 200) final_kwargs.setdefault('text', self.DEFAULT_RESP_BODY) self.requests_mock.register_uri(method, self.REDIRECT_CHAIN[-1], **final_kwargs) def assertResponse(self, resp): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.text, self.DEFAULT_RESP_BODY) def test_basic_get(self): session = client_session.Session() self.setup_redirects() resp = session.get(self.REDIRECT_CHAIN[-2]) self.assertResponse(resp) def test_basic_post_keeps_correct_method(self): session = client_session.Session() self.setup_redirects(method='POST', status_code=301) resp = session.post(self.REDIRECT_CHAIN[-2]) self.assertResponse(resp) def test_redirect_forever(self): session = client_session.Session(redirect=True) self.setup_redirects() resp = session.get(self.REDIRECT_CHAIN[0]) self.assertResponse(resp) self.assertTrue(len(resp.history), len(self.REDIRECT_CHAIN)) def test_no_redirect(self): session = client_session.Session(redirect=False) self.setup_redirects() resp = session.get(self.REDIRECT_CHAIN[0]) self.assertEqual(resp.status_code, 305) self.assertEqual(resp.url, self.REDIRECT_CHAIN[0]) def test_redirect_limit(self): self.setup_redirects() for i in (1, 2): session = client_session.Session(redirect=i) resp = session.get(self.REDIRECT_CHAIN[0]) self.assertEqual(resp.status_code, 305) self.assertEqual(resp.url, self.REDIRECT_CHAIN[i]) self.assertEqual(resp.text, self.DEFAULT_REDIRECT_BODY) def test_history_matches_requests(self): self.setup_redirects(status_code=301) session = client_session.Session(redirect=True) req_resp = requests.get(self.REDIRECT_CHAIN[0], allow_redirects=True) ses_resp = session.get(self.REDIRECT_CHAIN[0]) self.assertEqual(len(req_resp.history), len(ses_resp.history)) for r, s in zip(req_resp.history, ses_resp.history): self.assertEqual(r.url, s.url) self.assertEqual(r.status_code, s.status_code) class AuthPlugin(plugin.BaseAuthPlugin): """Very simple debug authentication plugin. Takes Parameters such that it can throw exceptions at the right times. """ TEST_TOKEN = utils.TestCase.TEST_TOKEN TEST_USER_ID = 'aUser' TEST_PROJECT_ID = 'aProject' SERVICE_URLS = { 'identity': {'public': 'http://identity-public:1111/v2.0', 'admin': 'http://identity-admin:1111/v2.0'}, 'compute': {'public': 'http://compute-public:2222/v1.0', 'admin': 'http://compute-admin:2222/v1.0'}, 'image': {'public': 'http://image-public:3333/v2.0', 'admin': 'http://image-admin:3333/v2.0'} } def __init__(self, token=TEST_TOKEN, invalidate=True): self.token = token self._invalidate = invalidate def get_token(self, session): return self.token def get_endpoint(self, session, service_type=None, interface=None, **kwargs): try: return self.SERVICE_URLS[service_type][interface] except (KeyError, AttributeError): return None def invalidate(self): return self._invalidate def get_user_id(self, session): return self.TEST_USER_ID def get_project_id(self, session): return self.TEST_PROJECT_ID class CalledAuthPlugin(plugin.BaseAuthPlugin): ENDPOINT = 'http://fakeendpoint/' USER_ID = uuid.uuid4().hex PROJECT_ID = uuid.uuid4().hex def __init__(self, invalidate=True): self.get_token_called = False self.get_endpoint_called = False self.endpoint_arguments = {} self.invalidate_called = False self.get_project_id_called = False self.get_user_id_called = False self._invalidate = invalidate def get_token(self, session): self.get_token_called = True return utils.TestCase.TEST_TOKEN def get_endpoint(self, session, **kwargs): self.get_endpoint_called = True self.endpoint_arguments = kwargs return self.ENDPOINT def invalidate(self): self.invalidate_called = True return self._invalidate def get_project_id(self, session, **kwargs): self.get_project_id_called = True return self.PROJECT_ID def get_user_id(self, session, **kwargs): self.get_user_id_called = True return self.USER_ID class SessionAuthTests(utils.TestCase): TEST_URL = 'http://127.0.0.1:5000/' TEST_JSON = {'hello': 'world'} def stub_service_url(self, service_type, interface, path, method='GET', **kwargs): base_url = AuthPlugin.SERVICE_URLS[service_type][interface] uri = "%s/%s" % (base_url.rstrip('/'), path.lstrip('/')) self.requests_mock.register_uri(method, uri, **kwargs) def test_auth_plugin_default_with_plugin(self): self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON) # if there is an auth_plugin then it should default to authenticated auth = AuthPlugin() sess = client_session.Session(auth=auth) resp = sess.get(self.TEST_URL) self.assertDictEqual(resp.json(), self.TEST_JSON) self.assertRequestHeaderEqual('X-Auth-Token', AuthPlugin.TEST_TOKEN) def test_auth_plugin_disable(self): self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON) auth = AuthPlugin() sess = client_session.Session(auth=auth) resp = sess.get(self.TEST_URL, authenticated=False) self.assertDictEqual(resp.json(), self.TEST_JSON) self.assertRequestHeaderEqual('X-Auth-Token', None) def test_service_type_urls(self): service_type = 'compute' interface = 'public' path = '/instances' status = 200 body = 'SUCCESS' self.stub_service_url(service_type=service_type, interface=interface, path=path, status_code=status, text=body) sess = client_session.Session(auth=AuthPlugin()) resp = sess.get(path, endpoint_filter={'service_type': service_type, 'interface': interface}) self.assertEqual(self.requests_mock.last_request.url, AuthPlugin.SERVICE_URLS['compute']['public'] + path) self.assertEqual(resp.text, body) self.assertEqual(resp.status_code, status) def test_service_url_raises_if_no_auth_plugin(self): sess = client_session.Session() self.assertRaises(exceptions.MissingAuthPlugin, sess.get, '/path', endpoint_filter={'service_type': 'compute', 'interface': 'public'}) def test_service_url_raises_if_no_url_returned(self): sess = client_session.Session(auth=AuthPlugin()) self.assertRaises(exceptions.EndpointNotFound, sess.get, '/path', endpoint_filter={'service_type': 'unknown', 'interface': 'public'}) def test_raises_exc_only_when_asked(self): # A request that returns a HTTP error should by default raise an # exception by default, if you specify raise_exc=False then it will not self.requests_mock.get(self.TEST_URL, status_code=401) sess = client_session.Session() self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL) resp = sess.get(self.TEST_URL, raise_exc=False) self.assertEqual(401, resp.status_code) def test_passed_auth_plugin(self): passed = CalledAuthPlugin() sess = client_session.Session() self.requests_mock.get(CalledAuthPlugin.ENDPOINT + 'path', status_code=200) endpoint_filter = {'service_type': 'identity'} # no plugin with authenticated won't work self.assertRaises(exceptions.MissingAuthPlugin, sess.get, 'path', authenticated=True) # no plugin with an endpoint filter won't work self.assertRaises(exceptions.MissingAuthPlugin, sess.get, 'path', authenticated=False, endpoint_filter=endpoint_filter) resp = sess.get('path', auth=passed, endpoint_filter=endpoint_filter) self.assertEqual(200, resp.status_code) self.assertTrue(passed.get_endpoint_called) self.assertTrue(passed.get_token_called) def test_passed_auth_plugin_overrides(self): fixed = CalledAuthPlugin() passed = CalledAuthPlugin() sess = client_session.Session(fixed) self.requests_mock.get(CalledAuthPlugin.ENDPOINT + 'path', status_code=200) resp = sess.get('path', auth=passed, endpoint_filter={'service_type': 'identity'}) self.assertEqual(200, resp.status_code) self.assertTrue(passed.get_endpoint_called) self.assertTrue(passed.get_token_called) self.assertFalse(fixed.get_endpoint_called) self.assertFalse(fixed.get_token_called) def test_requests_auth_plugin(self): sess = client_session.Session() requests_auth = object() FAKE_RESP = utils.TestResponse({'status_code': 200, 'text': 'resp'}) RESP = mock.Mock(return_value=FAKE_RESP) with mock.patch.object(sess.session, 'request', RESP) as mocked: sess.get(self.TEST_URL, requests_auth=requests_auth) mocked.assert_called_once_with('GET', self.TEST_URL, headers=mock.ANY, allow_redirects=mock.ANY, auth=requests_auth, verify=mock.ANY) def test_reauth_called(self): auth = CalledAuthPlugin(invalidate=True) sess = client_session.Session(auth=auth) self.requests_mock.get(self.TEST_URL, [{'text': 'Failed', 'status_code': 401}, {'text': 'Hello', 'status_code': 200}]) # allow_reauth=True is the default resp = sess.get(self.TEST_URL, authenticated=True) self.assertEqual(200, resp.status_code) self.assertEqual('Hello', resp.text) self.assertTrue(auth.invalidate_called) def test_reauth_not_called(self): auth = CalledAuthPlugin(invalidate=True) sess = client_session.Session(auth=auth) self.requests_mock.get(self.TEST_URL, [{'text': 'Failed', 'status_code': 401}, {'text': 'Hello', 'status_code': 200}]) self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL, authenticated=True, allow_reauth=False) self.assertFalse(auth.invalidate_called) def test_endpoint_override_overrides_filter(self): auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) override_base = 'http://mytest/' path = 'path' override_url = override_base + path resp_text = uuid.uuid4().hex self.requests_mock.get(override_url, text=resp_text) resp = sess.get(path, endpoint_override=override_base, endpoint_filter={'service_type': 'identity'}) self.assertEqual(resp_text, resp.text) self.assertEqual(override_url, self.requests_mock.last_request.url) self.assertTrue(auth.get_token_called) self.assertFalse(auth.get_endpoint_called) self.assertFalse(auth.get_user_id_called) self.assertFalse(auth.get_project_id_called) def test_endpoint_override_ignore_full_url(self): auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) path = 'path' url = self.TEST_URL + path resp_text = uuid.uuid4().hex self.requests_mock.get(url, text=resp_text) resp = sess.get(url, endpoint_override='http://someother.url', endpoint_filter={'service_type': 'identity'}) self.assertEqual(resp_text, resp.text) self.assertEqual(url, self.requests_mock.last_request.url) self.assertTrue(auth.get_token_called) self.assertFalse(auth.get_endpoint_called) self.assertFalse(auth.get_user_id_called) self.assertFalse(auth.get_project_id_called) def test_endpoint_override_does_id_replacement(self): auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) override_base = 'http://mytest/%(project_id)s/%(user_id)s' path = 'path' replacements = {'user_id': CalledAuthPlugin.USER_ID, 'project_id': CalledAuthPlugin.PROJECT_ID} override_url = override_base % replacements + '/' + path resp_text = uuid.uuid4().hex self.requests_mock.get(override_url, text=resp_text) resp = sess.get(path, endpoint_override=override_base, endpoint_filter={'service_type': 'identity'}) self.assertEqual(resp_text, resp.text) self.assertEqual(override_url, self.requests_mock.last_request.url) self.assertTrue(auth.get_token_called) self.assertTrue(auth.get_user_id_called) self.assertTrue(auth.get_project_id_called) self.assertFalse(auth.get_endpoint_called) def test_endpoint_override_fails_to_replace_if_none(self): # The token_endpoint plugin doesn't know user_id or project_id auth = token_endpoint.Token(uuid.uuid4().hex, uuid.uuid4().hex) sess = client_session.Session(auth=auth) override_base = 'http://mytest/%(project_id)s' e = self.assertRaises(ValueError, sess.get, '/path', endpoint_override=override_base, endpoint_filter={'service_type': 'identity'}) self.assertIn('project_id', str(e)) override_base = 'http://mytest/%(user_id)s' e = self.assertRaises(ValueError, sess.get, '/path', endpoint_override=override_base, endpoint_filter={'service_type': 'identity'}) self.assertIn('user_id', str(e)) def test_endpoint_override_fails_to_do_unknown_replacement(self): auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) override_base = 'http://mytest/%(unknown_id)s' e = self.assertRaises(AttributeError, sess.get, '/path', endpoint_override=override_base, endpoint_filter={'service_type': 'identity'}) self.assertIn('unknown_id', str(e)) def test_user_and_project_id(self): auth = AuthPlugin() sess = client_session.Session(auth=auth) self.assertEqual(auth.TEST_USER_ID, sess.get_user_id()) self.assertEqual(auth.TEST_PROJECT_ID, sess.get_project_id()) def test_logger_object_passed(self): logger = logging.getLogger(uuid.uuid4().hex) logger.setLevel(logging.DEBUG) logger.propagate = False io = six.StringIO() handler = logging.StreamHandler(io) logger.addHandler(handler) auth = AuthPlugin() sess = client_session.Session(auth=auth) response = uuid.uuid4().hex self.stub_url('GET', text=response, headers={'Content-Type': 'text/html'}) resp = sess.get(self.TEST_URL, logger=logger) self.assertEqual(response, resp.text) output = io.getvalue() self.assertIn(self.TEST_URL, output) self.assertIn(response, output) self.assertNotIn(self.TEST_URL, self.logger.output) self.assertNotIn(response, self.logger.output) class AdapterTest(utils.TestCase): SERVICE_TYPE = uuid.uuid4().hex SERVICE_NAME = uuid.uuid4().hex INTERFACE = uuid.uuid4().hex REGION_NAME = uuid.uuid4().hex USER_AGENT = uuid.uuid4().hex VERSION = uuid.uuid4().hex TEST_URL = CalledAuthPlugin.ENDPOINT def _create_loaded_adapter(self): auth = CalledAuthPlugin() sess = client_session.Session() return adapter.Adapter(sess, auth=auth, service_type=self.SERVICE_TYPE, service_name=self.SERVICE_NAME, interface=self.INTERFACE, region_name=self.REGION_NAME, user_agent=self.USER_AGENT, version=self.VERSION) def _verify_endpoint_called(self, adpt): self.assertEqual(self.SERVICE_TYPE, adpt.auth.endpoint_arguments['service_type']) self.assertEqual(self.SERVICE_NAME, adpt.auth.endpoint_arguments['service_name']) self.assertEqual(self.INTERFACE, adpt.auth.endpoint_arguments['interface']) self.assertEqual(self.REGION_NAME, adpt.auth.endpoint_arguments['region_name']) self.assertEqual(self.VERSION, adpt.auth.endpoint_arguments['version']) def test_setting_variables_on_request(self): response = uuid.uuid4().hex self.stub_url('GET', text=response) adpt = self._create_loaded_adapter() resp = adpt.get('/') self.assertEqual(resp.text, response) self._verify_endpoint_called(adpt) self.assertTrue(adpt.auth.get_token_called) self.assertRequestHeaderEqual('User-Agent', self.USER_AGENT) def test_setting_variables_on_get_endpoint(self): adpt = self._create_loaded_adapter() url = adpt.get_endpoint() self.assertEqual(self.TEST_URL, url) self._verify_endpoint_called(adpt) def test_legacy_binding(self): key = uuid.uuid4().hex val = uuid.uuid4().hex response = json.dumps({key: val}) self.stub_url('GET', text=response) auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) adpt = adapter.LegacyJsonAdapter(sess, service_type=self.SERVICE_TYPE, user_agent=self.USER_AGENT) resp, body = adpt.get('/') self.assertEqual(self.SERVICE_TYPE, auth.endpoint_arguments['service_type']) self.assertEqual(resp.text, response) self.assertEqual(val, body[key]) def test_legacy_binding_non_json_resp(self): response = uuid.uuid4().hex self.stub_url('GET', text=response, headers={'Content-Type': 'text/html'}) auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) adpt = adapter.LegacyJsonAdapter(sess, service_type=self.SERVICE_TYPE, user_agent=self.USER_AGENT) resp, body = adpt.get('/') self.assertEqual(self.SERVICE_TYPE, auth.endpoint_arguments['service_type']) self.assertEqual(resp.text, response) self.assertIsNone(body) def test_methods(self): sess = client_session.Session() adpt = adapter.Adapter(sess) url = 'http://url' for method in ['get', 'head', 'post', 'put', 'patch', 'delete']: with mock.patch.object(adpt, 'request') as m: getattr(adpt, method)(url) m.assert_called_once_with(url, method.upper()) def test_setting_endpoint_override(self): endpoint_override = 'http://overrideurl' path = '/path' endpoint_url = endpoint_override + path auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) adpt = adapter.Adapter(sess, endpoint_override=endpoint_override) response = uuid.uuid4().hex self.requests_mock.get(endpoint_url, text=response) resp = adpt.get(path) self.assertEqual(response, resp.text) self.assertEqual(endpoint_url, self.requests_mock.last_request.url) self.assertEqual(endpoint_override, adpt.get_endpoint()) def test_adapter_invalidate(self): auth = CalledAuthPlugin() sess = client_session.Session() adpt = adapter.Adapter(sess, auth=auth) adpt.invalidate() self.assertTrue(auth.invalidate_called) def test_adapter_get_token(self): auth = CalledAuthPlugin() sess = client_session.Session() adpt = adapter.Adapter(sess, auth=auth) self.assertEqual(self.TEST_TOKEN, adpt.get_token()) self.assertTrue(auth.get_token_called) def test_adapter_connect_retries(self): retries = 2 sess = client_session.Session() adpt = adapter.Adapter(sess, connect_retries=retries) self.stub_url('GET', exc=requests.exceptions.ConnectionError()) with mock.patch('time.sleep') as m: self.assertRaises(exceptions.ConnectionError, adpt.get, self.TEST_URL) self.assertEqual(retries, m.call_count) # we count retries so there will be one initial request + 2 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_user_and_project_id(self): auth = AuthPlugin() sess = client_session.Session() adpt = adapter.Adapter(sess, auth=auth) self.assertEqual(auth.TEST_USER_ID, adpt.get_user_id()) self.assertEqual(auth.TEST_PROJECT_ID, adpt.get_project_id()) def test_logger_object_passed(self): logger = logging.getLogger(uuid.uuid4().hex) logger.setLevel(logging.DEBUG) logger.propagate = False io = six.StringIO() handler = logging.StreamHandler(io) logger.addHandler(handler) auth = AuthPlugin() sess = client_session.Session(auth=auth) adpt = adapter.Adapter(sess, auth=auth, logger=logger) response = uuid.uuid4().hex self.stub_url('GET', text=response, headers={'Content-Type': 'text/html'}) resp = adpt.get(self.TEST_URL, logger=logger) self.assertEqual(response, resp.text) output = io.getvalue() self.assertIn(self.TEST_URL, output) self.assertIn(response, output) self.assertNotIn(self.TEST_URL, self.logger.output) self.assertNotIn(response, self.logger.output) def test_unknown_connection_error(self): self.stub_url('GET', exc=requests.exceptions.RequestException) self.assertRaises(exceptions.UnknownConnectionError, client_session.Session().request, self.TEST_URL, 'GET') class TCPKeepAliveAdapterTest(utils.TestCase): def setUp(self): super(TCPKeepAliveAdapterTest, self).setUp() self.init_poolmanager = self.patch( client_session.requests.adapters.HTTPAdapter, 'init_poolmanager') self.constructor = self.patch( client_session.TCPKeepAliveAdapter, '__init__', lambda self: None) def test_init_poolmanager_with_requests_lesser_than_2_4_1(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 0)) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then no socket_options are given self.init_poolmanager.assert_called_once_with(1, 2, 3) def test_init_poolmanager_with_basic_options(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) socket = self.patch_socket_with_options( ['IPPROTO_TCP', 'TCP_NODELAY', 'SOL_SOCKET', 'SO_KEEPALIVE']) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then no socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=[ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]) def test_init_poolmanager_with_tcp_keepidle(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) socket = self.patch_socket_with_options( ['IPPROTO_TCP', 'TCP_NODELAY', 'SOL_SOCKET', 'SO_KEEPALIVE', 'TCP_KEEPIDLE']) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=[ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)]) def test_init_poolmanager_with_tcp_keepcnt(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) socket = self.patch_socket_with_options( ['IPPROTO_TCP', 'TCP_NODELAY', 'SOL_SOCKET', 'SO_KEEPALIVE', 'TCP_KEEPCNT']) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=[ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4)]) def test_init_poolmanager_with_tcp_keepintvl(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) socket = self.patch_socket_with_options( ['IPPROTO_TCP', 'TCP_NODELAY', 'SOL_SOCKET', 'SO_KEEPALIVE', 'TCP_KEEPINTVL']) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=[ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15)]) def test_init_poolmanager_with_given_optionsl(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) given_adapter = client_session.TCPKeepAliveAdapter() given_options = object() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3, socket_options=given_options) # then socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=given_options) def patch_socket_with_options(self, option_names): # to mock socket module with exactly the attributes I want I create # a class with that attributes socket = type('socket', (object,), {name: 'socket.' + name for name in option_names}) return self.patch(client_session, 'socket', socket) def patch(self, target, name, *args, **kwargs): context = mock.patch.object(target, name, *args, **kwargs) patch = context.start() self.addCleanup(context.stop) return patch keystoneauth-2.4.1/keystoneauth1/tests/unit/test_token_endpoint.py000066400000000000000000000051111271245473000256220ustar00rootroot00000000000000# 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 testtools import matchers from keystoneauth1.loading._plugins import admin_token as loader from keystoneauth1 import session from keystoneauth1.tests.unit import utils from keystoneauth1 import token_endpoint class TokenEndpointTest(utils.TestCase): TEST_TOKEN = 'aToken' TEST_URL = 'http://server/prefix' def test_basic_case(self): self.requests_mock.get(self.TEST_URL, text='body') a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) s = session.Session(auth=a) data = s.get(self.TEST_URL, authenticated=True) self.assertEqual(data.text, 'body') self.assertRequestHeaderEqual('X-Auth-Token', self.TEST_TOKEN) def test_basic_endpoint_case(self): self.stub_url('GET', ['p'], text='body') a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) s = session.Session(auth=a) data = s.get('/p', authenticated=True, endpoint_filter={'service': 'identity'}) self.assertEqual(self.TEST_URL, a.get_endpoint(s)) self.assertEqual('body', data.text) self.assertRequestHeaderEqual('X-Auth-Token', self.TEST_TOKEN) def test_token_endpoint_user_id(self): a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) s = session.Session() # we can't know this information about this sort of plugin self.assertIsNone(a.get_user_id(s)) self.assertIsNone(a.get_project_id(s)) class AdminTokenTest(utils.TestCase): def test_token_endpoint_options(self): opt_names = [opt.name for opt in loader.AdminToken().get_options()] self.assertThat(opt_names, matchers.HasLength(2)) self.assertIn('token', opt_names) self.assertIn('endpoint', opt_names) def test_token_endpoint_deprecated_options(self): endpoint_opt = [ opt for opt in loader.AdminToken().get_options() if opt.name == 'endpoint'][0] opt_names = [opt.name for opt in endpoint_opt.deprecated] self.assertEqual(['url'], opt_names) keystoneauth-2.4.1/keystoneauth1/tests/unit/test_utils.py000066400000000000000000000014271271245473000237500ustar00rootroot00000000000000# 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 keystoneauth1 import _utils class UtilsTests(testtools.TestCase): def test_get_logger(self): self.assertEqual('keystoneauth.tests.unit.test_utils', _utils.get_logger(__name__).name) keystoneauth-2.4.1/keystoneauth1/tests/unit/utils.py000066400000000000000000000111151271245473000227040ustar00rootroot00000000000000# 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 as jsonutils import logging import time import uuid import fixtures import requests from requests_mock.contrib import fixture import six from six.moves.urllib import parse as urlparse import testtools class TestCase(testtools.TestCase): TEST_DOMAIN_ID = uuid.uuid4().hex TEST_DOMAIN_NAME = uuid.uuid4().hex TEST_GROUP_ID = uuid.uuid4().hex TEST_ROLE_ID = uuid.uuid4().hex TEST_TENANT_ID = uuid.uuid4().hex TEST_TENANT_NAME = uuid.uuid4().hex TEST_TOKEN = uuid.uuid4().hex TEST_TRUST_ID = uuid.uuid4().hex TEST_USER = uuid.uuid4().hex 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)) fixtures.MockPatchObject(time, 'time', lambda: 1234) self.requests_mock = self.useFixture(fixture.Fixture()) def stub_url(self, method, parts=None, base_url=None, json=None, **kwargs): if not base_url: base_url = self.TEST_URL if json: kwargs['text'] = jsonutils.dumps(json) headers = kwargs.setdefault('headers', {}) headers['Content-Type'] = 'application/json' if parts: url = '/'.join([p.strip('/') for p in [base_url] + parts]) else: url = base_url url = url.replace("/?", "?") self.requests_mock.register_uri(method, url, **kwargs) def assertRequestBodyIs(self, body=None, json=None): last_request_body = self.requests_mock.last_request.body if json: val = jsonutils.loads(last_request_body) self.assertEqual(json, val) elif body: self.assertEqual(body, last_request_body) def assertQueryStringIs(self, qs=''): """Verify the QueryString matches what is expected. The qs parameter should be of the format \'foo=bar&abc=xyz\' """ expected = urlparse.parse_qs(qs, keep_blank_values=True) parts = urlparse.urlparse(self.requests_mock.last_request.url) querystring = urlparse.parse_qs(parts.query, keep_blank_values=True) self.assertEqual(expected, querystring) def assertQueryStringContains(self, **kwargs): """Verify the query string contains the expected parameters. This method is used to verify that the query string for the most recent request made contains all the parameters provided as ``kwargs``, and that the value of each parameter contains the value for the kwarg. If the value for the kwarg is an empty string (''), then all that's verified is that the parameter is present. """ parts = urlparse.urlparse(self.requests_mock.last_request.url) qs = urlparse.parse_qs(parts.query, keep_blank_values=True) for k, v in six.iteritems(kwargs): self.assertIn(k, qs) self.assertIn(v, qs[k]) def assertRequestHeaderEqual(self, name, val): """Verify that the last request made contains a header and its value The request must have already been made. """ headers = self.requests_mock.last_request.headers self.assertEqual(headers.get(name), val) class TestResponse(requests.Response): """Class used to wrap requests.Response. This provides 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 keystoneauth-2.4.1/keystoneauth1/token_endpoint.py000066400000000000000000000025231271245473000224460ustar00rootroot00000000000000# 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 keystoneauth1 import plugin class Token(plugin.BaseAuthPlugin): """A provider that will always use the given token and endpoint. This is really only useful for testing and in certain CLI cases where you have a known endpoint and admin token that you want to use. """ def __init__(self, endpoint, token): # NOTE(jamielennox): endpoint is reserved for when plugins # can be used to provide that information self.endpoint = endpoint self.token = token def get_token(self, session): return self.token def get_endpoint(self, session, **kwargs): """Return the supplied endpoint. Using this plugin the same endpoint is returned regardless of the parameters passed to the plugin. """ return self.endpoint keystoneauth-2.4.1/releasenotes/000077500000000000000000000000001271245473000167375ustar00rootroot00000000000000keystoneauth-2.4.1/releasenotes/notes/000077500000000000000000000000001271245473000200675ustar00rootroot00000000000000keystoneauth-2.4.1/releasenotes/notes/.placeholder000066400000000000000000000000001271245473000223400ustar00rootroot00000000000000keystoneauth-2.4.1/releasenotes/notes/ksa_2.2.0-81145229d4b43043.yaml000066400000000000000000000004031271245473000241520ustar00rootroot00000000000000--- fixes: - > [`bug 1527131 `_] Do not provide socket values for OSX and Windows. other: - Added a betamax fixture for keystoneauth sessions. - Added a RFC 7231 compliant user agent string. keystoneauth-2.4.1/releasenotes/source/000077500000000000000000000000001271245473000202375ustar00rootroot00000000000000keystoneauth-2.4.1/releasenotes/source/_static/000077500000000000000000000000001271245473000216655ustar00rootroot00000000000000keystoneauth-2.4.1/releasenotes/source/_static/.placeholder000066400000000000000000000000001271245473000241360ustar00rootroot00000000000000keystoneauth-2.4.1/releasenotes/source/_templates/000077500000000000000000000000001271245473000223745ustar00rootroot00000000000000keystoneauth-2.4.1/releasenotes/source/_templates/.placeholder000066400000000000000000000000001271245473000246450ustar00rootroot00000000000000keystoneauth-2.4.1/releasenotes/source/conf.py000066400000000000000000000217651271245473000215510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # 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. # keystoneauth Release Notes documentation build configuration file, created # by sphinx-quickstart on Tue Nov 3 17:40:50 2015. # # 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. # 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.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'oslosphinx', 'reno.sphinxext', ] # 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-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'keystoneauth Release Notes' copyright = u'2015, Keystone Developers' # 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. # # The short X.Y version. import pbr.version keystone_version = pbr.version.VersionInfo('keystoneauth') # The full version, including alpha/beta/rc tags. release = keystone_version.version_string_with_vcs() # The short X.Y version. version = keystone_version.canonical_version_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 patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # 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 = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # 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 # " v 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'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # 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_domain_indices = 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, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'keystoneauthReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'keystoneauthReleaseNotes.tex', u'keystoneauth Release Notes Documentation', u'Keystone Developers', '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 # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'keystoneauthreleasenotes', u'keystoneauth Release Notes Documentation', [u'Keystone Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'keystoneauthReleaseNotes', u'keystoneauth Release Notes Documentation', u'Keystone Developers', 'keystoneauthReleaseNotes', 'Authentication plugins for the OpenStack Identity service.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False keystoneauth-2.4.1/releasenotes/source/index.rst000066400000000000000000000002031271245473000220730ustar00rootroot00000000000000============================ keystoneauth Release Notes ============================ .. toctree:: :maxdepth: 1 unreleased keystoneauth-2.4.1/releasenotes/source/unreleased.rst000066400000000000000000000001601271245473000231150ustar00rootroot00000000000000============================== Current Series Release Notes ============================== .. release-notes:: keystoneauth-2.4.1/requirements.txt000066400000000000000000000005611271245473000175340ustar00rootroot00000000000000# 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. pbr>=1.6 # Apache-2.0 iso8601>=0.1.9 # MIT positional>=1.0.1 # Apache-2.0 requests!=2.9.0,>=2.8.1 # Apache-2.0 six>=1.9.0 # MIT stevedore>=1.5.0 # Apache-2.0 keystoneauth-2.4.1/setup.cfg000066400000000000000000000034311271245473000160700ustar00rootroot00000000000000[metadata] name = keystoneauth1 summary = Authentication Library for OpenStack Identity description-file = README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org home-page = http://www.openstack.org/ 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 :: 3 Programming Language :: Python :: 3.4 [files] packages = keystoneauth1 [extras] kerberos = requests-kerberos>=0.6:python_version=='2.7' or python_version=='2.6' # MIT saml2 = lxml>=2.3 # BSD betamax = betamax>=0.5.1 # Apache-2.0 fixtures<2.0,>=1.3.1 # Apache-2.0/BSD mock>=1.2 # BSD [entry_points] keystoneauth1.plugin = password = keystoneauth1.loading._plugins.identity.generic:Password token = keystoneauth1.loading._plugins.identity.generic:Token admin_token = keystoneauth1.loading._plugins.admin_token:AdminToken v2password = keystoneauth1.loading._plugins.identity.v2:Password v2token = keystoneauth1.loading._plugins.identity.v2:Token v3password = keystoneauth1.loading._plugins.identity.v3:Password v3token = keystoneauth1.loading._plugins.identity.v3:Token v3oidcpassword = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectPassword v3oidcauthcode = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAuthorizationCode [build_sphinx] source-dir = doc/source build-dir = doc/build all_files = 1 [pbr] warnerrors = True autodoc_tree_index_modules = True [upload_sphinx] upload-dir = doc/build/html [wheel] universal = 1 keystoneauth-2.4.1/setup.py000066400000000000000000000020041271245473000157540ustar00rootroot00000000000000# 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>=1.8'], pbr=True) keystoneauth-2.4.1/test-requirements.txt000066400000000000000000000014201271245473000205040ustar00rootroot00000000000000# 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.11,>=0.10.0 flake8-docstrings==0.2.1.post1 # MIT coverage>=3.6 # Apache-2.0 discover # BSD fixtures<2.0,>=1.3.1 # Apache-2.0/BSD mock>=1.2 # BSD oslo.config>=3.7.0 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 oslo.utils>=3.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 os-testr>=0.4.1 # Apache-2.0 betamax>=0.5.1 # Apache-2.0 pycrypto>=2.6 # Public Domain reno>=0.1.1 # Apache2 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testresources>=0.2.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT keystoneauth-2.4.1/tox.ini000066400000000000000000000036311271245473000155640ustar00rootroot00000000000000[tox] minversion = 1.6 skipsdist = True envlist = py34,py27,pep8,releasenotes [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 .[kerberos,saml2,betamax] commands = ostestr {posargs} [testenv:pep8] commands = flake8 [testenv:venv] commands = {posargs} [testenv:cover] commands = python setup.py testr --coverage --testr-args='{posargs}' [testenv:debug] commands = oslo_debug_helper -t keystoneauth1/tests {posargs} [flake8] # H405: multi line docstring summary not separated with an empty line # D100: Missing docstring in public module # D101: Missing docstring in public class # D102: Missing docstring in public method # D103: Missing docstring in public function # D104: Missing docstring in public package # D105: Missing docstring in magic method # D200: One-line docstring should fit on one line with quotes # D202: No blank lines allowed after function docstring # D203: 1 blank required before class docstring. # D204: 1 blank required after class docstring # D205: Blank line required between one-line summary and description. # D208: Docstring is over-indented # D211: No blank lines allowed before class docstring # D301: Use r”“” if any backslashes in a docstring # D400: First line should end with a period. # D401: First line should be in imperative mood. ignore = H405,D100,D101,D102,D103,D104,D105,D200,D202,D203,D204,D205,D208,D211,D301,D400,D401 show-source = True exclude = .venv,.tox,dist,doc,*egg,build,*openstack/common* [testenv:docs] commands= python setup.py build_sphinx [testenv:releasenotes] commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [hacking] local-check-factory = keystoneauth1.hacking.checks.factory