Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: I3ca69746ffe73b7c01c3066476589fac8e16a5e9
This commit is contained in:
parent
ccbd20ed9c
commit
1068cb6f7f
@ -1,7 +0,0 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = keystoneauth1
|
||||
omit = keystoneauth1/tests/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
25
.gitignore
vendored
25
.gitignore
vendored
@ -1,25 +0,0 @@
|
||||
.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
|
@ -1,4 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/keystoneauth.git
|
6
.mailmap
6
.mailmap
@ -1,6 +0,0 @@
|
||||
# Format is:
|
||||
# <preferred e-mail> <other e-mail 1>
|
||||
# <preferred e-mail> <other e-mail 2>
|
||||
<mr.alex.meade@gmail.com> <hatboy112@yahoo.com>
|
||||
<mr.alex.meade@gmail.com> <alex.meade@rackspace.com>
|
||||
<morgan.fainberg@gmail.com> <m@metacloud.com>
|
@ -1,4 +0,0 @@
|
||||
[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
|
@ -1,18 +0,0 @@
|
||||
If you would like to contribute to the development of OpenStack,
|
||||
you must follow the steps documented at:
|
||||
|
||||
https://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:
|
||||
|
||||
https://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
|
24
HACKING.rst
24
HACKING.rst
@ -1,24 +0,0 @@
|
||||
Keystone Style Commandments
|
||||
===========================
|
||||
|
||||
- Step 1: Read the OpenStack Style Commandments
|
||||
https://docs.openstack.org/hacking/latest/
|
||||
- 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 https://wiki.openstack.org/testr. If you'd like to learn more
|
||||
in depth:
|
||||
|
||||
https://testtools.readthedocs.org/
|
||||
https://testrepository.readthedocs.org/
|
209
LICENSE
209
LICENSE
@ -1,209 +0,0 @@
|
||||
Copyright (c) 2009 Jacob Kaplan-Moss - initial codebase (< v2.1)
|
||||
Copyright (c) 2011 Rackspace - OpenStack extensions (>= v2.1)
|
||||
Copyright (c) 2011 Nebula, Inc - Keystone refactor (>= v2.7)
|
||||
All rights reserved.
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
--- License for 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.
|
14
README
Normal file
14
README
Normal file
@ -0,0 +1,14 @@
|
||||
This project is no longer maintained.
|
||||
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
For ongoing work on maintaining OpenStack packages in the Debian
|
||||
distribution, please see the Debian OpenStack packaging team at
|
||||
https://wiki.debian.org/OpenStack/.
|
||||
|
||||
For any further questions, please email
|
||||
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
35
README.rst
35
README.rst
@ -1,35 +0,0 @@
|
||||
========================
|
||||
Team and repository tags
|
||||
========================
|
||||
|
||||
.. image:: https://governance.openstack.org/badges/keystoneauth.svg
|
||||
:target: https://governance.openstack.org/reference/tags/index.html
|
||||
|
||||
.. Change things from this point on
|
||||
|
||||
============
|
||||
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: https://docs.openstack.org/keystoneauth/latest/
|
||||
* Source: https://git.openstack.org/cgit/openstack/keystoneauth
|
||||
* Bugs: https://bugs.launchpad.net/keystoneauth
|
@ -1,8 +0,0 @@
|
||||
# This is a cross-platform list tracking distribution packages needed for install and tests;
|
||||
# see https://docs.openstack.org/infra/bindep/ for additional information.
|
||||
|
||||
build-essential [platform:dpkg test]
|
||||
python-dev [platform:dpkg test]
|
||||
python-devel [platform:rpm test]
|
||||
libkrb5-dev [platform:dpkg test]
|
||||
krb5-devel [platform:rpm test]
|
1
doc/.gitignore
vendored
1
doc/.gitignore
vendored
@ -1 +0,0 @@
|
||||
build/
|
90
doc/Makefile
90
doc/Makefile
@ -1,90 +0,0 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXSOURCE = source
|
||||
PAPER =
|
||||
BUILDDIR = build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE)
|
||||
|
||||
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/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."
|
@ -1,93 +0,0 @@
|
||||
# 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 inspect
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers import rst
|
||||
from docutils.parsers.rst import directives
|
||||
from docutils.statemachine import ViewList
|
||||
from sphinx.util.nodes import nested_parse_with_titles
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
|
||||
class ListAuthPluginsDirective(rst.Directive):
|
||||
"""Present a simple list of the plugins in a namespace."""
|
||||
|
||||
option_spec = {
|
||||
'class': directives.class_option,
|
||||
'overline-style': directives.single_char_or_unicode,
|
||||
'underline-style': directives.single_char_or_unicode,
|
||||
}
|
||||
|
||||
has_content = True
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
return self.state.document.settings.env.app
|
||||
|
||||
def report_load_failure(mgr, ep, err):
|
||||
self.app.warn(u'Failed to load %s: %s' % (ep.module_name, err))
|
||||
|
||||
def display_plugin(self, ext):
|
||||
overline_style = self.options.get('overline-style', '')
|
||||
underline_style = self.options.get('underline-style', '=')
|
||||
|
||||
if overline_style:
|
||||
yield overline_style * len(ext.name)
|
||||
|
||||
yield ext.name
|
||||
|
||||
if underline_style:
|
||||
yield underline_style * len(ext.name)
|
||||
|
||||
yield "\n"
|
||||
|
||||
doc = inspect.getdoc(ext.obj)
|
||||
if doc:
|
||||
yield doc
|
||||
yield "\n"
|
||||
yield "------"
|
||||
|
||||
yield "\n"
|
||||
|
||||
for opt in ext.obj.get_options():
|
||||
yield ":%s: %s" % (opt.name, opt.help)
|
||||
|
||||
yield "\n"
|
||||
|
||||
def run(self):
|
||||
mgr = extension.ExtensionManager(
|
||||
'keystoneauth1.plugin',
|
||||
on_load_failure_callback=self.report_load_failure,
|
||||
invoke_on_load=True,
|
||||
)
|
||||
|
||||
result = ViewList()
|
||||
|
||||
for name in sorted(mgr.names()):
|
||||
for line in self.display_plugin(mgr[name]):
|
||||
for l in line.splitlines():
|
||||
result.append(l, mgr[name].entry_point.module_name)
|
||||
|
||||
# Parse what we have into a new section.
|
||||
node = nodes.section()
|
||||
node.document = self.state.document
|
||||
nested_parse_with_titles(self.state, result, node)
|
||||
|
||||
return node.children
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.info('loading keystoneauth1 plugins')
|
||||
app.add_directive('list-auth-plugins', ListAuthPluginsDirective)
|
@ -1,303 +0,0 @@
|
||||
======================
|
||||
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
|
||||
=================
|
||||
|
||||
Keystoneauth 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.identity.v3.TOTPMethod`: Authenticate against
|
||||
a V3 identity service using Time-Based One-Time Password (TOTP).
|
||||
- :py:class:`~keystoneauth1.identity.v3.TokenlessAuth`: Authenticate against
|
||||
a V3 identity service using tokenless authentication.
|
||||
- :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.identity.v3.TOTP`: Authenticate using
|
||||
only a :py:class:`~keystoneauth1.identity.v3.TOTPMethod`.
|
||||
- :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``.
|
||||
|
||||
Federation
|
||||
==========
|
||||
|
||||
The following V3 plugins are provided to support federation:
|
||||
|
||||
- :py:class:`~keystoneauth1.extras.kerberos.MappedKerberos`: Federated (mapped)
|
||||
Kerberos.
|
||||
- :py:class:`~keystoneauth1.extras._saml2.v3.Password`: SAML2 password
|
||||
authentication.
|
||||
- :py:class:`~keystoneauth1.identity.v3.Keystone2Keystone`: Keystone to
|
||||
Keystone Federation.
|
||||
- :py:class:`~keystoneauth1.identity.v3:OpenIDConnectAccessToken`: Plugin to
|
||||
reuse an existing OpenID Connect access token.
|
||||
- :py:class:`~keystoneauth1.identity.v3:OpenIDConnectAuthorizationCode`: OpenID
|
||||
Connect Authorization Code grant type.
|
||||
- :py:class:`~keystoneauth1.identity.v3:OpenIDConnectClientCredentials`: OpenID
|
||||
Connect Client Credentials grant type.
|
||||
- :py:class:`~keystoneauth1.identity.v3:OpenIDConnectPassword`: OpenID Connect
|
||||
Resource Owner Password Credentials grant type.
|
||||
|
||||
|
||||
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 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.extras.oauth1.V3OAuth1`.
|
||||
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.extras import oauth1
|
||||
>>> from keystoneauth1 import session
|
||||
>>> a = oauth1.V3OAuth1('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)
|
||||
|
||||
|
||||
Tokenless Auth
|
||||
==============
|
||||
|
||||
A plugin for tokenless authentication also exists. It provides a means to
|
||||
authorize client operations within the Identity server by using an X.509
|
||||
TLS client certificate without having to issue a token. We provide a
|
||||
tokenless authentication plugin at:
|
||||
|
||||
- :class:`~keystoneauth1.identity.v3.TokenlessAuth`
|
||||
|
||||
It is mostly used by service clients for token validation and here is
|
||||
an example of how this plugin would be used in practice::
|
||||
|
||||
>>> from keystoneauth1 import session
|
||||
>>> from keystoneauth1.identity import v3
|
||||
>>> auth = v3.TokenlessAuth(auth_url='https://keystone:5000/v3',
|
||||
... domain_name='my_service_domain')
|
||||
>>> sess = session.Session(
|
||||
... auth=auth,
|
||||
... cert=('/opt/service_client.crt',
|
||||
... '/opt/service_client.key'),
|
||||
... verify='/opt/ca.crt')
|
||||
|
||||
|
||||
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`
|
||||
- v3fedkerb: :py:class:`keystoneauth1.extras.kerberos.MappedKerberos`
|
||||
- v3kerberos: :py:class:`keystoneauth1.extras.kerberos.Kerberos`
|
||||
- v3oauth1: :py:class:`keystoneauth1.extras.oauth1.v3.OAuth1`
|
||||
- v3oidcaccesstoken: :py:class:`keystoneauth1.identity.v3:OpenIDConnectAccessToken`
|
||||
- v3oidcauthcode: :py:class:`keystoneauth1.identity.v3:OpenIDConnectAuthorizationCode`
|
||||
- v3oidcclientcredentials: :py:class:`keystoneauth1.identity.v3:OpenIDConnectClientCredentials`
|
||||
- v3oidcpassword: :py:class:`keystoneauth1.identity.v3:OpenIDConnectPassword`
|
||||
- v3samlpassword: :py:class:`keystoneauth1.extras._saml2.v3.Password`
|
||||
- v3tokenlessauth: :py:class:`keystoneauth1.identity.v3.TokenlessAuth`
|
||||
- v3totp: :py:class:`keystoneauth1.identity.v3.TOTP`
|
||||
|
||||
|
||||
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.
|
@ -1,237 +0,0 @@
|
||||
# -*- 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 sys
|
||||
|
||||
import pbr.version
|
||||
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'..', '..')))
|
||||
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',
|
||||
'openstackdocstheme',
|
||||
'ext.list_plugins',
|
||||
]
|
||||
|
||||
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'
|
||||
html_theme = 'openstackdocs'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
#html_static_path = ['static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_use_modindex = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = ''
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = '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 = 'https://docs.openstack.org/python-keystoneclient/latest/'
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('http://docs.python.org/', None),
|
||||
'osloconfig': ('https://docs.openstack.org/oslo.config/latest/', None),
|
||||
'keystoneclient': (ksc, None),
|
||||
}
|
||||
|
||||
# -- Options for openstackdocstheme -------------------------------------------
|
||||
repository_name = 'openstack/keystoneauth'
|
||||
bug_project = 'keystoneauth'
|
||||
bug_tag = 'doc'
|
@ -1,73 +0,0 @@
|
||||
======
|
||||
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 <https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-extras-optional-features-with-their-own-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.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
All extras plugins follow the pattern:
|
||||
|
||||
1. import plugin module
|
||||
2. instantiate the plugin
|
||||
3. call get_token method of the plugin passing it a session object
|
||||
to get a token
|
||||
|
||||
Kerberos
|
||||
--------
|
||||
|
||||
Get domain-scoped token using
|
||||
:py:class:`~keystoneauth1.extras.kerberos.Kerberos`::
|
||||
|
||||
from keystoneauth1.extras import kerberos
|
||||
from keystoneauth1 import session
|
||||
|
||||
plugin = kerberos.Kerberos('http://example.com:5000/v3')
|
||||
sess = session.Session(plugin)
|
||||
token = plugin.get_token(sess)
|
||||
|
||||
Get unscoped federated token::
|
||||
|
||||
from keystoneauth1.extras import kerberos
|
||||
from keystoneauth1 import session
|
||||
|
||||
plugin = kerberos.MappedKerberos(
|
||||
auth_url='http://example.com:5000/v3', protocol='example_protocol',
|
||||
identity_provider='example_identity_provider')
|
||||
|
||||
sess = session.Session()
|
||||
token = plugin.get_token(sess)
|
||||
|
||||
Get project scoped federated token::
|
||||
|
||||
from keystoneauth1.extras import kerberos
|
||||
from keystoneauth1 import session
|
||||
|
||||
plugin = kerberos.MappedKerberos(
|
||||
auth_url='http://example.com:5000/v3', protocol='example_protocol',
|
||||
identity_provider='example_identity_provider',
|
||||
project_id='example_project_id')
|
||||
|
||||
sess = session.Session()
|
||||
token = plugin.get_token(sess)
|
||||
project_id = plugin.get_project_id(sess)
|
@ -1 +0,0 @@
|
||||
.. include:: ../../ChangeLog
|
@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.27.20101213.0545 (20101213.0545)
|
||||
-->
|
||||
<!-- Title: AuthComp Pages: 1 -->
|
||||
<svg width="510pt" height="118pt"
|
||||
viewBox="0.00 0.00 510.00 118.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph1" class="graph" transform="scale(1 1) rotate(0) translate(4 114)">
|
||||
<title>AuthComp</title>
|
||||
<polygon fill="white" stroke="white" points="-4,5 -4,-114 507,-114 507,5 -4,5"/>
|
||||
<!-- AuthComp -->
|
||||
<g id="node2" class="node"><title>AuthComp</title>
|
||||
<polygon fill="#fdefe3" stroke="#c00000" points="292,-65 194,-65 194,-25 292,-25 292,-65"/>
|
||||
<text text-anchor="middle" x="243" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">Auth</text>
|
||||
<text text-anchor="middle" x="243" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Component</text>
|
||||
</g>
|
||||
<!-- Reject -->
|
||||
<!-- AuthComp->Reject -->
|
||||
<g id="edge3" class="edge"><title>AuthComp->Reject</title>
|
||||
<path fill="none" stroke="black" d="M193.933,-51.2787C157.514,-55.939 108.38,-62.2263 73.8172,-66.649"/>
|
||||
<polygon fill="black" stroke="black" points="73.0637,-63.2168 63.5888,-67.9578 73.9522,-70.1602 73.0637,-63.2168"/>
|
||||
<text text-anchor="middle" x="129" y="-97.4" font-family="Times,serif" font-size="14.00">Reject</text>
|
||||
<text text-anchor="middle" x="129" y="-82.4" font-family="Times,serif" font-size="14.00">Unauthenticated</text>
|
||||
<text text-anchor="middle" x="129" y="-67.4" font-family="Times,serif" font-size="14.00">Requests</text>
|
||||
</g>
|
||||
<!-- Service -->
|
||||
<g id="node6" class="node"><title>Service</title>
|
||||
<polygon fill="#d1ebf1" stroke="#1f477d" points="502,-65 408,-65 408,-25 502,-25 502,-65"/>
|
||||
<text text-anchor="middle" x="455" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">OpenStack</text>
|
||||
<text text-anchor="middle" x="455" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Service</text>
|
||||
</g>
|
||||
<!-- AuthComp->Service -->
|
||||
<g id="edge5" class="edge"><title>AuthComp->Service</title>
|
||||
<path fill="none" stroke="black" d="M292.17,-45C323.626,-45 364.563,-45 397.52,-45"/>
|
||||
<polygon fill="black" stroke="black" points="397.917,-48.5001 407.917,-45 397.917,-41.5001 397.917,-48.5001"/>
|
||||
<text text-anchor="middle" x="350" y="-77.4" font-family="Times,serif" font-size="14.00">Forward</text>
|
||||
<text text-anchor="middle" x="350" y="-62.4" font-family="Times,serif" font-size="14.00">Authenticated</text>
|
||||
<text text-anchor="middle" x="350" y="-47.4" font-family="Times,serif" font-size="14.00">Requests</text>
|
||||
</g>
|
||||
<!-- Start -->
|
||||
<!-- Start->AuthComp -->
|
||||
<g id="edge7" class="edge"><title>Start->AuthComp</title>
|
||||
<path fill="none" stroke="black" d="M59.1526,-21.4745C90.4482,-25.4792 142.816,-32.1802 183.673,-37.4084"/>
|
||||
<polygon fill="black" stroke="black" points="183.43,-40.9057 193.793,-38.7034 184.318,-33.9623 183.43,-40.9057"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.9 KiB |
@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.27.20101213.0545 (20101213.0545)
|
||||
-->
|
||||
<!-- Title: AuthCompDelegate Pages: 1 -->
|
||||
<svg width="588pt" height="104pt"
|
||||
viewBox="0.00 0.00 588.00 104.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph1" class="graph" transform="scale(1 1) rotate(0) translate(4 100)">
|
||||
<title>AuthCompDelegate</title>
|
||||
<polygon fill="white" stroke="white" points="-4,5 -4,-100 585,-100 585,5 -4,5"/>
|
||||
<!-- AuthComp -->
|
||||
<g id="node2" class="node"><title>AuthComp</title>
|
||||
<polygon fill="#fdefe3" stroke="#c00000" points="338,-65 240,-65 240,-25 338,-25 338,-65"/>
|
||||
<text text-anchor="middle" x="289" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">Auth</text>
|
||||
<text text-anchor="middle" x="289" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Component</text>
|
||||
</g>
|
||||
<!-- Reject -->
|
||||
<!-- AuthComp->Reject -->
|
||||
<g id="edge3" class="edge"><title>AuthComp->Reject</title>
|
||||
<path fill="none" stroke="black" d="M239.6,-50.1899C191.406,-55.2531 118.917,-62.8686 73.5875,-67.6309"/>
|
||||
<polygon fill="black" stroke="black" points="73.0928,-64.1635 63.5132,-68.6893 73.8242,-71.1252 73.0928,-64.1635"/>
|
||||
<text text-anchor="middle" x="152" y="-83.4" font-family="Times,serif" font-size="14.00">Reject Requests</text>
|
||||
<text text-anchor="middle" x="152" y="-68.4" font-family="Times,serif" font-size="14.00">Indicated by the Service</text>
|
||||
</g>
|
||||
<!-- Service -->
|
||||
<g id="node6" class="node"><title>Service</title>
|
||||
<polygon fill="#d1ebf1" stroke="#1f477d" points="580,-65 486,-65 486,-25 580,-25 580,-65"/>
|
||||
<text text-anchor="middle" x="533" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">OpenStack</text>
|
||||
<text text-anchor="middle" x="533" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Service</text>
|
||||
</g>
|
||||
<!-- AuthComp->Service -->
|
||||
<g id="edge5" class="edge"><title>AuthComp->Service</title>
|
||||
<path fill="none" stroke="black" d="M338.009,-49.0804C344.065,-49.4598 350.172,-49.7828 356,-50 405.743,-51.8535 418.259,-51.9103 468,-50 470.523,-49.9031 473.101,-49.7851 475.704,-49.6504"/>
|
||||
<polygon fill="black" stroke="black" points="476.03,-53.1374 485.807,-49.0576 475.62,-46.1494 476.03,-53.1374"/>
|
||||
<text text-anchor="middle" x="412" y="-68.4" font-family="Times,serif" font-size="14.00">Forward Requests</text>
|
||||
<text text-anchor="middle" x="412" y="-53.4" font-family="Times,serif" font-size="14.00">with Identiy Status</text>
|
||||
</g>
|
||||
<!-- Service->AuthComp -->
|
||||
<g id="edge7" class="edge"><title>Service->AuthComp</title>
|
||||
<path fill="none" stroke="black" d="M495.062,-24.9037C486.397,-21.2187 477.064,-17.9304 468,-16 419.314,-5.63183 404.743,-5.9037 356,-16 349.891,-17.2653 343.655,-19.116 337.566,-21.2803"/>
|
||||
<polygon fill="black" stroke="black" points="336.234,-18.0426 328.158,-24.9003 338.748,-24.5757 336.234,-18.0426"/>
|
||||
<text text-anchor="middle" x="412" y="-33.4" font-family="Times,serif" font-size="14.00">Send Response OR</text>
|
||||
<text text-anchor="middle" x="412" y="-18.4" font-family="Times,serif" font-size="14.00">Reject Message</text>
|
||||
</g>
|
||||
<!-- Start -->
|
||||
<!-- Start->AuthComp -->
|
||||
<g id="edge9" class="edge"><title>Start->AuthComp</title>
|
||||
<path fill="none" stroke="black" d="M59.0178,-20.8384C99.2135,-25.0613 175.782,-33.1055 229.492,-38.7482"/>
|
||||
<polygon fill="black" stroke="black" points="229.265,-42.2435 239.576,-39.8076 229.997,-35.2818 229.265,-42.2435"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.5 KiB |
@ -1,49 +0,0 @@
|
||||
Common 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
|
||||
plugin-options
|
||||
|
||||
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: https://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
Run tests with ``tox``.
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
@ -1,102 +0,0 @@
|
||||
=============================
|
||||
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 handled separately
|
||||
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 should implement the base
|
||||
:py:class:`~keystoneauth1.loading.BaseLoader` class which is loaded when
|
||||
`--os-auth-type` is used. This class 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 entrypoints 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`.
|
||||
|
||||
Step-by-step migration example
|
||||
------------------------------
|
||||
|
||||
Add ``keystoneauth1`` to requirements.txt
|
||||
|
||||
In the code do the following change::
|
||||
|
||||
-from keystoneclient import auth
|
||||
+from keystoneauth1 import plugin
|
||||
|
||||
consequently::
|
||||
|
||||
-auth.BaseAuthPlugin
|
||||
+plugin.BaseAuthPlugin
|
||||
|
||||
To import service catalog::
|
||||
|
||||
-from keystoneclient import service_catalog
|
||||
+from keystoneauth1.access import service_catalog
|
||||
|
||||
To get url using service catalog *endpoint_type* parameter was changed to
|
||||
*interface*::
|
||||
|
||||
-service_catalog.ServiceCatalogV2(sc).service_catalog.url_for(..., endpoint_type=interface)
|
||||
+service_catalog.ServiceCatalogV2(sc).service_catalog.url_for(..., interface=interface)
|
||||
|
||||
Obtaining the session::
|
||||
|
||||
-from keystoneclient import session
|
||||
+from keystoneauth1 import loading as ks_loading
|
||||
|
||||
-_SESSION = session.Session.load_from_conf_options(
|
||||
-auth_plugin = auth.load_from_conf_options(conf, NEUTRON_GROUP)
|
||||
+_SESSION = ks_loading.load_session_from_conf_options(
|
||||
+auth_plugin = ks_loading.load_auth_from_conf_options(conf, NEUTRON_GROUP)
|
||||
|
||||
Mocking session for test purposes::
|
||||
|
||||
-@mock.patch('keystoneclient.session.Session')
|
||||
+@mock.patch('keystoneauth1.session.Session')
|
||||
|
||||
Token fixture imports haven't change much::
|
||||
|
||||
-from keystoneclient.fixture import V2Token
|
||||
+from keystoneauth1.fixture import V2Token
|
||||
|
@ -1,92 +0,0 @@
|
||||
==============
|
||||
Plugin Options
|
||||
==============
|
||||
|
||||
Using plugins via config file
|
||||
-----------------------------
|
||||
|
||||
When using the plugins via config file you define the plugin name as
|
||||
``auth_type``. The options of the plugin are then specified while replacing
|
||||
``-`` with ``_`` to be valid in configuration.
|
||||
|
||||
For example to use the password_ plugin in a config file you would specify:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[section]
|
||||
auth_url = http://keystone.example.com:5000/
|
||||
auth_type = password
|
||||
username = myuser
|
||||
password = mypassword
|
||||
project_name = myproject
|
||||
default_domain_name = mydomain
|
||||
|
||||
|
||||
Using plugins via CLI
|
||||
---------------------
|
||||
|
||||
When using auth plugins via CLI via ``os-client-config`` or ``shade`` you can
|
||||
specify parameters via environment configuration by using the pattern ``OS_``
|
||||
followed by the uppercase parameter name replacing ``-`` with ``_``.
|
||||
|
||||
For example to use the password_ plugin via environment variable you specify:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export OS_AUTH_TYPE=password
|
||||
export OS_AUTH_URL=http://keystone.example.com:5000/
|
||||
export OS_USERNAME=myuser
|
||||
export OS_PASSWORD=mypassword
|
||||
export OS_PROJECT_NAME=myproject
|
||||
export OS_DEFAULT_DOMAIN_NAME=mydomain
|
||||
|
||||
Specifying operations via CLI parameter will override the environment
|
||||
parameter. These are specified with the pattern ``--os-`` and the parameter
|
||||
name. Using the password_ example again:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
openstack --os-auth-type password \
|
||||
--os-auth-url http://keystone.example.com:5000/ \
|
||||
--os-username myuser \
|
||||
--os-password mypassword \
|
||||
--os-project-name myproject \
|
||||
--os-default-domain-name mydomain \
|
||||
operation
|
||||
|
||||
Additional loaders
|
||||
------------------
|
||||
|
||||
The configuration and CLI loaders are quite commonly used however similar
|
||||
concepts are found in other situations such as ``os-client-config`` in which
|
||||
you specify authentication and other cloud parameters in a ``clouds.yaml``
|
||||
file.
|
||||
|
||||
Loaders such as these use the same plugin options listed below, but via their
|
||||
own mechanism. In ``os-client-config`` the password_ plugin looks like:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
clouds:
|
||||
mycloud:
|
||||
auth_type: password
|
||||
auth:
|
||||
auth_url: http://keystone.example.com:5000/
|
||||
auth_type: password
|
||||
username: myuser
|
||||
password: mypassword
|
||||
project_name: myproject
|
||||
default_domain_name: mydomain
|
||||
|
||||
However different services may implement loaders in their own way and you
|
||||
should consult their relevant documentation. The same auth options will be
|
||||
available.
|
||||
|
||||
|
||||
Available Plugins
|
||||
-----------------
|
||||
|
||||
This is a listing of all included plugins and the options that they accept.
|
||||
Plugins are listed alphabetically and not in any order of priority.
|
||||
|
||||
.. list-auth-plugins::
|
@ -1,440 +0,0 @@
|
||||
==============
|
||||
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 <http://docs.python-requests.org>`_
|
||||
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 service and version discovery
|
||||
|
||||
Clients are not expected to have any knowledge of an identity token or any
|
||||
other form of identification credential. Service, endpoint, major version
|
||||
discovery and microversion support discovery are handled by the Session and
|
||||
plugins. Discovery information is automatically cached in memory, so the user
|
||||
need not worry about excessive use of discovery metadata.
|
||||
|
||||
|
||||
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 using keystoneclient to wrap a Session::
|
||||
|
||||
>>> 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 other OpenStack client libraries 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.
|
||||
|
||||
Major Version Discovery and Microversion Support
|
||||
------------------------------------------------
|
||||
|
||||
In OpenStack the root URLs of available services are distributed to the user
|
||||
in an object called the Service Catalog, which is part of the token they
|
||||
receive. Clients are expected to use the URLs from the Service Catalog rather
|
||||
than have them provided. The root URL of a given service is referred to as the
|
||||
`endpoint` of the service. The URL of a specific version of a service is
|
||||
referred to as a `versioned endpoint`. REST requests for a service are made
|
||||
against a given `versioned endpoint`.
|
||||
|
||||
The topic of Major API versions and microversions can be confusing. As
|
||||
`keystoneauth` provides facilities for discovery of versioned endpoints
|
||||
associated with a Major API Version and for fetching information about
|
||||
the microversions that versioned endpoint supports, it is important to be aware
|
||||
of the distinction between the two.
|
||||
|
||||
Conceptually the most important thing to understand is that a Major API Version
|
||||
describes the URL of a discrete versioned endpoint, while a given versioned
|
||||
endpoint might have properties that express that it supports a range of
|
||||
microversions.
|
||||
|
||||
When a user wants to make a REST request against a service, the user expresses
|
||||
the Major API version and the type of service so that the appropriate versioned
|
||||
endpoint can be found and used. For example, a user might request
|
||||
version 2 of the compute service from cloud.example.com and end up with a
|
||||
versioned endpoint of ``https://compute.example.com/v2``.
|
||||
|
||||
Each service provides a discovery document at the root of each versioned
|
||||
endpoint that contains information about that versioned endpoint. Each service
|
||||
also provides a document at the root of the unversioned endpoint that contains
|
||||
a list of the discovery documents for all of the available versioned endpoints.
|
||||
By examining these documents, it is possible to find the versioned endpoint
|
||||
that corresponds with the user's desired Major API version.
|
||||
|
||||
Each of those documents may also indicate that the given versioned endpoint
|
||||
supports microversions by listing a minimum and maximum microversion that it
|
||||
understands. As a result of having found the versioned endpoint for the
|
||||
requested Major API version, the user will also know which microversions,
|
||||
if any, may be used in requests to that versioned endpoint.
|
||||
|
||||
When a client makes REST requests to the Major API version's endpoint, the
|
||||
client can, optionally, on a request-by-request basis, include a header
|
||||
specifying that the individual request use the behavior defined by the given
|
||||
microversion. If a client does not request a microversion, the service will
|
||||
behave as if the minimum supported microversion was specified.
|
||||
|
||||
.. note: The changes that each microversion reflects are documented elsewhere
|
||||
and are not information provided by the discovery process.
|
||||
|
||||
The overall transaction then has three parts:
|
||||
|
||||
* What is the endpoint for a given Major API version of a given service?
|
||||
* What are the minimum and maximum microversions supported at that endpoint?
|
||||
* Which one of that range of microversions, if any, does the user want to use
|
||||
for a given request?
|
||||
|
||||
`keystoneauth` provides facilities for discovering the endpoint for a given
|
||||
Major API of a given service, as well as reporting the available microversion
|
||||
ranges that endpoint supports, if any.
|
||||
|
||||
More information is available in the `API-WG Specs`_ on the topics of
|
||||
`Microversions`_ and `Consuming the Catalog`_.
|
||||
|
||||
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 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('/users',
|
||||
endpoint_filter={'service_type': 'identity',
|
||||
'interface': 'admin',
|
||||
'region_name': 'myregion',
|
||||
'min_version': '2.0',
|
||||
'max_version': '3.4',
|
||||
'discover_versions': False})
|
||||
|
||||
.. note:: The min_version and max_version arguments in this example indicate
|
||||
acceptable range for finding the endpoint for the given Major API
|
||||
versions. They are in the endpoint_filter, they are not requesting
|
||||
the call to ``/users`` be made at a specific microversion.
|
||||
|
||||
`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. Can also be a list, in which case the
|
||||
first matching interface will be used. Valid values are:
|
||||
|
||||
- ``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.
|
||||
|
||||
version
|
||||
the minimum version, restricted to a given Major API. For instance, a
|
||||
`version` of ``2.2`` will match ``2.2`` and ``2.3`` but not ``2.1`` or
|
||||
``3.0``. Mutually exclusive with `min_version` and `max_version`.
|
||||
|
||||
min_version
|
||||
the minimum version of a given API, intended to be used as the lower bound of
|
||||
a range with `max_version`. See `max_version` for examples. Mutually
|
||||
exclusive with `version`.
|
||||
|
||||
max_version
|
||||
the maximum version of a given API, intended to be used as the upper bound of
|
||||
a range with `min_version`. For example::
|
||||
|
||||
'min_version': '2.2',
|
||||
'max_version': '3.3'
|
||||
|
||||
will match ``2.2``, ``2.10``, ``3.0``, and ``3.3``, but not ``1.42``,
|
||||
``2.1``, or ``3.20``. Mutually exclusive with `version`.
|
||||
|
||||
.. note:: version, min_version and max_version are all used to help determine
|
||||
the endpoint for a given Major API version of a service.
|
||||
|
||||
discover_versions
|
||||
whether or not version discovery should be run, even if not strictly
|
||||
necessary. It is often possible to fulfill an endpoint request purely
|
||||
from the catalog, meaning the version discovery API is a potentially
|
||||
wasted additional call. However, it's possible that running discovery
|
||||
instead of inference is desired. Defaults to ``True``.
|
||||
|
||||
All version arguments (`version`, `min_version` and `max_version`) can
|
||||
be given as:
|
||||
|
||||
* string: ``'2.0'``
|
||||
* int: ``2``
|
||||
* float: ``2.0``
|
||||
* tuple of ints: ``(2, 0)``
|
||||
|
||||
`version` and `max_version` can also be given the string ``latest``, which
|
||||
indicates that the highest available version should be used.
|
||||
|
||||
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.
|
||||
|
||||
If you want to further limit your service discovery by allowing experimental
|
||||
APIs or disallowing deprecated APIs, you can use the ``allow`` parameter::
|
||||
|
||||
>>> resp = session.get('/<project-id>/volumes',
|
||||
endpoint_filter={'service_type': 'volume',
|
||||
'interface': 'public',
|
||||
'version': 1},
|
||||
allow={'allow_deprecated': False})
|
||||
|
||||
The discoverable types of endpoints that `allow` can recognize are:
|
||||
|
||||
- `allow_deprecated`: Allow deprecated version endpoints.
|
||||
|
||||
- `allow_experimental`: Allow experimental version endpoints.
|
||||
|
||||
- `allow_unknown`: Allow endpoints with an unrecognised status.
|
||||
|
||||
The Session object creates a valid request by determining the URL matching the
|
||||
filters and appending it to the provided path. 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. A specific mechanism may not know how to interpret
|
||||
certain arguments in which case it may 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.
|
||||
|
||||
Using Adapters
|
||||
--------------
|
||||
|
||||
If the developer would prefer not to provide `endpoint_filter` with every API
|
||||
call, a :class:`keystoneauth1.adapter.Adapter` can be created. The `Adapter`
|
||||
constructor takes the same arguments as `endpoint_filter`, as well as a
|
||||
`Session`. An `Adapter` behaves much like a `Session`, with the same REST
|
||||
methods, but is "mounted" on the endpoint that would be found by
|
||||
`endpoint_filter`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
adapter = keystoneauth1.adapter.Adapter(
|
||||
session=session,
|
||||
service_type='volume',
|
||||
interface='public',
|
||||
version=1)
|
||||
response = adapter.get('/volumes')
|
||||
|
||||
As with ``endpoint_filter`` on a Session, the ``version``, ``min_version``
|
||||
and ``max_version`` parameters exist to help determine the appropriate
|
||||
endpoint for a Major API of a service.
|
||||
|
||||
Endpoint Metadata
|
||||
-----------------
|
||||
|
||||
Both :class:`keystoneauth1.adapter.Adapter` and
|
||||
:class:`keystoneauth1.session.Session` have a method for getting metadata about
|
||||
the endpoint found for a given service: ``get_endpoint_data``.
|
||||
|
||||
On the :class:`keystoneauth1.session.Session` it takes the same arguments as
|
||||
`endpoint_filter`.
|
||||
|
||||
On the :class:`keystoneauth1.adapter.Adapter` it does not take arguments, as
|
||||
it returns the information for the Endpoint the Adapter is mounted on.
|
||||
|
||||
``get_endpoint_data`` returns an :class:`keystoneauth1.discovery.EndpointData`
|
||||
object. This object can be used to find information about the Endpoint,
|
||||
including which major `api_version` was found, or which `interface` in case
|
||||
of ranges, lists of input values or ``latest`` version.
|
||||
|
||||
It can also be used to determine the `min_microversion` and `max_microversion`
|
||||
supported by the API. If an API does not support microversions, the values for
|
||||
both will be ``None``. It will also contain values for `next_min_version` and
|
||||
`not_before` if they exist for the endpoint, or ``None`` if they do not. The
|
||||
:class:`keystoneauth1.discovery.EndpointData` object will always contain
|
||||
microversion related attributes regardless of whether the REST document does
|
||||
or not.
|
||||
|
||||
``get_endpoint_data`` makes use of the same cache as the rest of the discovery
|
||||
process, so calling it should incur no undue expense. By default it will make
|
||||
at least one version discovery call so that it can fetch microversion metadata.
|
||||
If the user knows a service does not support microversions and is merely
|
||||
curious as to which major version was discovered, `discover_versions` can be
|
||||
set to `False` to prevent fetching microversion metadata.
|
||||
|
||||
Requesting a Microversion
|
||||
-------------------------
|
||||
|
||||
A user who wants to specify a microversion for a given request can pass it to
|
||||
the ``microversion`` parameter of the `request` method on the
|
||||
:class:`keystoneauth1.session.Session` object, or the
|
||||
:class:`keystoneauth1.adapter.Adapter` object. This will cause `keystoneauth`
|
||||
to pass the appropriate header to the service informing the service of the
|
||||
microversion the user wants.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
resp = session.get('/volumes',
|
||||
microversion='3.15',
|
||||
endpoint_filter={'service_type': 'volume',
|
||||
'interface': 'public',
|
||||
'min_version': '3',
|
||||
'max_version': 'latest'})
|
||||
|
||||
If the user is using a :class:`keystoneauth1.adapter.Adapter`, the
|
||||
`service_type`, which is a part of the data sent in the microversion header,
|
||||
will be taken from the Adapter's `service_type`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
adapter = keystoneauth1.adapter.Adapter(
|
||||
session=session,
|
||||
service_type='compute',
|
||||
interface='public',
|
||||
min_version='2.1')
|
||||
response = adapter.get('/servers', microversion='2.38')
|
||||
|
||||
The user can also provide a ``default_microversion`` parameter to the Adapter
|
||||
constructor which will be used on all requests where an explicit microversion
|
||||
is not requested.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
adapter = keystoneauth1.adapter.Adapter(
|
||||
session=session,
|
||||
service_type='compute',
|
||||
interface='public',
|
||||
min_version='2.1',
|
||||
default_microversion='2.38')
|
||||
response = adapter.get('/servers')
|
||||
|
||||
If the user is using a :class:`keystoneauth1.session.Session`, the
|
||||
`service_type` will be taken from the `service_type` in `endpoint_filter`.
|
||||
|
||||
If the `service_type` is the incorrect value to use for the microversion header
|
||||
for the service in question, the parameter `microversion_service_type` can be
|
||||
given. For instance, although keystoneauth already knows about Cinder, the
|
||||
`service_type` for Cinder is ``block-storage`` but the microversion header
|
||||
expects ``volume``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Interactions with cinder do not need to explicitly override the
|
||||
# microversion_service_type - it is only being used as an example for the
|
||||
# use of the parameter.
|
||||
resp = session.get('/volumes',
|
||||
microversion='3.15',
|
||||
microversion_service_type='volume',
|
||||
endpoint_filter={'service_type': 'block-storage',
|
||||
'interface': 'public',
|
||||
'min_version': '3',
|
||||
'max_version': 'latest'})
|
||||
|
||||
|
||||
.. _API-WG Specs: http://specs.openstack.org/openstack/api-wg/
|
||||
.. _Consuming the Catalog: http://specs.openstack.org/openstack/api-wg/guidelines/consuming-catalog.html
|
||||
.. _Microversions: http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html#version-discovery
|
||||
|
@ -1,16 +0,0 @@
|
||||
# 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()
|
@ -1,83 +0,0 @@
|
||||
# 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):
|
||||
r"""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):
|
||||
r"""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
|
||||
|
||||
|
||||
# Detect if running on the Windows Subsystem for Linux
|
||||
try:
|
||||
with open('/proc/version', 'r') as f:
|
||||
is_windows_linux_subsystem = 'microsoft' in f.read().lower()
|
||||
except IOError:
|
||||
is_windows_linux_subsystem = False
|
@ -1,19 +0,0 @@
|
||||
# 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')
|
@ -1,755 +0,0 @@
|
||||
# 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):
|
||||
"""Determine 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):
|
||||
"""Return true if the auth token has a service catalog.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def auth_token(self):
|
||||
"""Return 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):
|
||||
"""Return the token expiration (as datetime object).
|
||||
|
||||
:returns: datetime
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def issued(self):
|
||||
"""Return the token issue time (as datetime object).
|
||||
|
||||
:returns: datetime
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
"""Return 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):
|
||||
"""Return the user id associated with the auth request.
|
||||
|
||||
:returns: str
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def user_domain_id(self):
|
||||
"""Return the user's domain id associated with the auth request.
|
||||
|
||||
:returns: str
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def user_domain_name(self):
|
||||
"""Return the user's domain name associated with the auth request.
|
||||
|
||||
:returns: str
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def role_ids(self):
|
||||
"""Return 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):
|
||||
"""Return 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):
|
||||
"""Return 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):
|
||||
"""Return 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):
|
||||
"""Return 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):
|
||||
"""Return 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):
|
||||
"""Return true if the auth token was scoped to a tenant (project).
|
||||
|
||||
:returns: bool
|
||||
"""
|
||||
return bool(self.project_id)
|
||||
|
||||
@property
|
||||
def domain_scoped(self):
|
||||
"""Return true if the auth token was scoped to a domain.
|
||||
|
||||
:returns: bool
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def trust_id(self):
|
||||
"""Return 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):
|
||||
"""Return 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):
|
||||
"""Return 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):
|
||||
"""Return 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):
|
||||
"""Return 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):
|
||||
"""Return the project's domain id associated with the auth request.
|
||||
|
||||
:returns: str
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def project_domain_name(self):
|
||||
"""Return 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):
|
||||
"""Return true if federation was used to get the token.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def is_admin_project(self):
|
||||
"""Return true if the current project scope is the admin project.
|
||||
|
||||
For backwards compatibility purposes if there is nothing specified in
|
||||
the token we always assume we are in the admin project, so this will
|
||||
default to True.
|
||||
|
||||
: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()
|
||||
|
||||
@property
|
||||
def project_is_domain(self):
|
||||
"""Return if a project act as a domain.
|
||||
|
||||
:returns: bool
|
||||
"""
|
||||
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_is_domain(self):
|
||||
return False
|
||||
|
||||
@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 is_admin_project(self):
|
||||
return True
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def is_admin_project(self):
|
||||
return self._data.get('token', {}).get('is_admin_project', True)
|
||||
|
||||
@_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_is_domain(self):
|
||||
return self._data['token']['is_domain']
|
||||
|
||||
@_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']
|
@ -1,512 +0,0 @@
|
||||
# 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
|
||||
import copy
|
||||
|
||||
from positional import positional
|
||||
import six
|
||||
|
||||
from keystoneauth1 import discover
|
||||
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
|
||||
|
||||
def _normalize_endpoints(self, endpoints):
|
||||
"""Translate endpoint description dicts into v3 form.
|
||||
|
||||
Takes a raw endpoint description from the catalog and changes
|
||||
it to be in v3 format. It also saves a copy of the data in
|
||||
raw_endpoint so that it can be returned by methods that expect the
|
||||
actual original data.
|
||||
|
||||
:param list endpoints: List of endpoint description dicts
|
||||
|
||||
:returns: List of endpoint description dicts in v3 format
|
||||
"""
|
||||
new_endpoints = []
|
||||
for endpoint in endpoints:
|
||||
raw_endpoint = endpoint.copy()
|
||||
new_endpoint = endpoint.copy()
|
||||
new_endpoint['raw_endpoint'] = raw_endpoint
|
||||
new_endpoints.append(new_endpoint)
|
||||
return new_endpoints
|
||||
|
||||
def _denormalize_endpoints(self, endpoints):
|
||||
"""Return original endpoint description dicts.
|
||||
|
||||
Takes a list of EndpointData objects and returns the original
|
||||
dict that was returned from the catalog.
|
||||
|
||||
:param list endpoints: List of `keystoneauth1.discover.EndpointData`
|
||||
|
||||
:returns: List of endpoint description dicts in original catalog format
|
||||
"""
|
||||
return [endpoint.raw_endpoint for endpoint in endpoints]
|
||||
|
||||
def normalize_catalog(self):
|
||||
"""Return the catalog normalized into v3 format."""
|
||||
catalog = []
|
||||
for service in copy.deepcopy(self._catalog):
|
||||
if 'type' not in service:
|
||||
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. Set to None so
|
||||
# that checks will naturally work.
|
||||
service.setdefault('name', None)
|
||||
|
||||
# NOTE(jamielennox): there is no such thing as a service_id in v2
|
||||
# similarly to service_name.
|
||||
service.setdefault('id', None)
|
||||
|
||||
service['endpoints'] = self._normalize_endpoints(
|
||||
service.get('endpoints', []))
|
||||
|
||||
for endpoint in service['endpoints']:
|
||||
endpoint['region_name'] = self._get_endpoint_region(endpoint)
|
||||
endpoint.setdefault('id', None)
|
||||
catalog.append(service)
|
||||
return catalog
|
||||
|
||||
def _get_interface_list(self, interface):
|
||||
if not interface:
|
||||
return []
|
||||
if not isinstance(interface, list):
|
||||
interface = [interface]
|
||||
return [self.normalize_interface(i) for i in interface]
|
||||
|
||||
@positional()
|
||||
def get_endpoints_data(self, service_type=None, interface=None,
|
||||
region_name=None, service_name=None,
|
||||
service_id=None, endpoint_id=None):
|
||||
"""Fetch and filter endpoint data 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.
|
||||
|
||||
Valid interface types: `public` or `publicURL`,
|
||||
`internal` or `internalURL`,
|
||||
`admin` or 'adminURL`
|
||||
|
||||
:param string service_type: Service type of the endpoint.
|
||||
:param interface: Type of endpoint. Can be a single value or a list
|
||||
of values. If it's a list of values, they will be
|
||||
looked for in order of preference.
|
||||
: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: a list of matching EndpointData objects
|
||||
:rtype: list(`keystoneauth1.discover.EndpointData`)
|
||||
|
||||
:returns: a dict, keyed by service_type, of lists of EndpointData
|
||||
"""
|
||||
interfaces = self._get_interface_list(interface)
|
||||
|
||||
matching_endpoints = {}
|
||||
|
||||
for service in self.normalize_catalog():
|
||||
|
||||
if service_type and service_type != service['type']:
|
||||
continue
|
||||
|
||||
if (service_name and service['name'] and
|
||||
service_name != service['name']):
|
||||
continue
|
||||
|
||||
if (service_id and service['id'] and
|
||||
service_id != service['id']):
|
||||
continue
|
||||
|
||||
matching_endpoints.setdefault(service['type'], [])
|
||||
|
||||
for endpoint in service.get('endpoints', []):
|
||||
if interfaces and endpoint['interface'] not in interfaces:
|
||||
continue
|
||||
if region_name and region_name != endpoint['region_name']:
|
||||
continue
|
||||
if endpoint_id and endpoint_id != endpoint['id']:
|
||||
continue
|
||||
if not endpoint['url']:
|
||||
continue
|
||||
|
||||
matching_endpoints[service['type']].append(
|
||||
discover.EndpointData(
|
||||
catalog_url=endpoint['url'],
|
||||
service_type=service['type'],
|
||||
service_name=service['name'],
|
||||
service_id=service['id'],
|
||||
interface=endpoint['interface'],
|
||||
region_name=endpoint['region_name'],
|
||||
endpoint_id=endpoint['id'],
|
||||
raw_endpoint=endpoint['raw_endpoint']))
|
||||
|
||||
if not interfaces:
|
||||
return matching_endpoints
|
||||
|
||||
ret = {}
|
||||
for service_type, endpoints in matching_endpoints.items():
|
||||
if not endpoints:
|
||||
ret[service_type] = []
|
||||
continue
|
||||
matches_by_interface = {}
|
||||
for endpoint in endpoints:
|
||||
matches_by_interface.setdefault(endpoint.interface, [])
|
||||
matches_by_interface[endpoint.interface].append(endpoint)
|
||||
best_interface = [i for i in interfaces
|
||||
if i in matches_by_interface.keys()][0]
|
||||
ret[service_type] = matches_by_interface[best_interface]
|
||||
|
||||
return ret
|
||||
|
||||
@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 endpoint data 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.
|
||||
|
||||
Returns a dict keyed by service_type with a list of endpoint dicts
|
||||
"""
|
||||
endpoints_data = self.get_endpoints_data(
|
||||
service_type=service_type, interface=interface,
|
||||
region_name=region_name, service_name=service_name,
|
||||
service_id=service_id, endpoint_id=endpoint_id)
|
||||
endpoints = {}
|
||||
for service_type, data in endpoints_data.items():
|
||||
endpoints[service_type] = self._denormalize_endpoints(data)
|
||||
return endpoints
|
||||
|
||||
@positional()
|
||||
def get_endpoint_data_list(self, service_type=None, interface='public',
|
||||
region_name=None, service_name=None,
|
||||
service_id=None, endpoint_id=None):
|
||||
"""Fetch a flat list of matching EndpointData objects.
|
||||
|
||||
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.
|
||||
|
||||
Valid interface types: `public` or `publicURL`,
|
||||
`internal` or `internalURL`,
|
||||
`admin` or 'adminURL`
|
||||
|
||||
:param string service_type: Service type of the endpoint.
|
||||
:param interface: Type of endpoint. Can be a single value or a list
|
||||
of values. If it's a list of values, they will be
|
||||
looked for in order of preference.
|
||||
: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: a list of matching EndpointData objects
|
||||
:rtype: list(`keystoneauth1.discover.EndpointData`)
|
||||
"""
|
||||
endpoints = self.get_endpoints_data(service_type=service_type,
|
||||
interface=interface,
|
||||
region_name=region_name,
|
||||
service_name=service_name,
|
||||
service_id=service_id,
|
||||
endpoint_id=endpoint_id)
|
||||
return [endpoint for data in endpoints.values() for endpoint in data]
|
||||
|
||||
@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 urls of endpoints from the service catalog for a particular
|
||||
endpoint attribute. If no attribute is given, return the url of the
|
||||
first endpoint of the specified type.
|
||||
|
||||
Valid interface types: `public` or `publicURL`,
|
||||
`internal` or `internalURL`,
|
||||
`admin` or 'adminURL`
|
||||
|
||||
:param string service_type: Service type of the endpoint.
|
||||
:param interface: Type of endpoint. Can be a single value or a list
|
||||
of values. If it's a list of values, they will be
|
||||
looked for in order of preference.
|
||||
: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
|
||||
"""
|
||||
endpoints = self.get_endpoint_data_list(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])
|
||||
|
||||
@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 interface types: `public` or `publicURL`,
|
||||
`internal` or `internalURL`,
|
||||
`admin` or 'adminURL`
|
||||
|
||||
:param string service_type: Service type of the endpoint.
|
||||
:param interface: Type of endpoint. Can be a single value or a list
|
||||
of values. If it's a list of values, they will be
|
||||
looked for in order of preference.
|
||||
: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.
|
||||
"""
|
||||
return self.endpoint_data_for(service_type=service_type,
|
||||
interface=interface,
|
||||
region_name=region_name,
|
||||
service_name=service_name,
|
||||
service_id=service_id,
|
||||
endpoint_id=endpoint_id).url
|
||||
|
||||
@positional()
|
||||
def endpoint_data_for(self, service_type=None, interface='public',
|
||||
region_name=None, service_name=None,
|
||||
service_id=None, endpoint_id=None):
|
||||
"""Fetch endpoint data from the service catalog.
|
||||
|
||||
Fetch the specified endpoint data from the service catalog for
|
||||
a particular endpoint attribute. If no attribute is given, return
|
||||
the first endpoint of the specified type.
|
||||
|
||||
Valid interface types: `public` or `publicURL`,
|
||||
`internal` or `internalURL`,
|
||||
`admin` or 'adminURL`
|
||||
|
||||
:param string service_type: Service type of the endpoint.
|
||||
:param interface: Type of endpoint. Can be a single value or a list
|
||||
of values. If it's a list of values, they will be
|
||||
looked for in order of preference.
|
||||
: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.')
|
||||
|
||||
endpoint_data_list = self.get_endpoint_data_list(
|
||||
service_type=service_type,
|
||||
interface=interface,
|
||||
region_name=region_name,
|
||||
service_name=service_name,
|
||||
service_id=service_id,
|
||||
endpoint_id=endpoint_id)
|
||||
|
||||
if endpoint_data_list:
|
||||
return endpoint_data_list[0]
|
||||
|
||||
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 += 'URL'
|
||||
|
||||
return interface
|
||||
|
||||
def is_interface_match(self, endpoint, interface):
|
||||
return interface in endpoint
|
||||
|
||||
def _normalize_endpoints(self, endpoints):
|
||||
"""Translate endpoint description dicts into v3 form.
|
||||
|
||||
Takes a raw endpoint description from the catalog and changes
|
||||
it to be in v3 format. It also saves a copy of the data in
|
||||
raw_endpoint so that it can be returned by methods that expect the
|
||||
actual original data.
|
||||
|
||||
:param list endpoints: List of endpoint description dicts
|
||||
|
||||
:returns: List of endpoint description dicts in v3 format
|
||||
"""
|
||||
new_endpoints = []
|
||||
for endpoint in endpoints:
|
||||
raw_endpoint = endpoint.copy()
|
||||
interface_urls = {}
|
||||
interface_keys = [key for key in endpoint.keys()
|
||||
if key.endswith('URL')]
|
||||
for key in interface_keys:
|
||||
interface = self.normalize_interface(key)
|
||||
interface_urls[interface] = endpoint.pop(key)
|
||||
for interface, url in interface_urls.items():
|
||||
new_endpoint = endpoint.copy()
|
||||
new_endpoint['interface'] = interface
|
||||
new_endpoint['url'] = url
|
||||
# Save the actual endpoint for ease of later reconstruction
|
||||
new_endpoint['raw_endpoint'] = raw_endpoint
|
||||
new_endpoints.append(new_endpoint)
|
||||
return new_endpoints
|
||||
|
||||
def _denormalize_endpoints(self, endpoints):
|
||||
"""Return original endpoint description dicts.
|
||||
|
||||
Takes a list of EndpointData objects and returns the original
|
||||
dict that was returned from the catalog.
|
||||
|
||||
:param list endpoints: List of `keystoneauth1.discover.EndpointData`
|
||||
|
||||
:returns: List of endpoint description dicts in original catalog format
|
||||
"""
|
||||
raw_endpoints = super(ServiceCatalogV2, self)._denormalize_endpoints(
|
||||
endpoints)
|
||||
# The same raw endpoint content will be in the list once for each
|
||||
# v2 endpoint_type entry. We only need one of them in the resulting
|
||||
# list. So keep a list of the string versions.
|
||||
seen = {}
|
||||
endpoints = []
|
||||
for endpoint in raw_endpoints:
|
||||
if str(endpoint) in seen:
|
||||
continue
|
||||
seen[str(endpoint)] = True
|
||||
endpoints.append(endpoint)
|
||||
return 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
|
@ -1,44 +0,0 @@
|
||||
# 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')
|
@ -1,466 +0,0 @@
|
||||
# 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 warnings
|
||||
|
||||
from positional import positional
|
||||
|
||||
from keystoneauth1 import session
|
||||
|
||||
|
||||
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.
|
||||
|
||||
version, min_version, max_version and default_microversion can all be
|
||||
given either as a string or a tuple.
|
||||
|
||||
: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 version: The minimum version restricted to a given Major API.
|
||||
Mutually exclusive with min_version and max_version.
|
||||
(optional)
|
||||
: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
|
||||
:param dict allow: Extra filters to pass when discovering API versions.
|
||||
(optional)
|
||||
:param dict additional_headers: Additional headers that should be attached
|
||||
to every request passing through the
|
||||
adapter. Headers of the same name specified
|
||||
per request will take priority.
|
||||
:param str client_name: The name of the client that created the adapter.
|
||||
This will be used to create the user_agent.
|
||||
:param str client_version: The version of the client that created the
|
||||
adapter. This will be used to create the
|
||||
user_agent.
|
||||
:param bool allow_version_hack: Allow keystoneauth to hack up catalog
|
||||
URLS to support older schemes.
|
||||
(optional, default True)
|
||||
:param str global_request_id: A global_request_id (in the form of
|
||||
``req-$uuid``) that will be passed on all
|
||||
requests. Enables cross project request id
|
||||
tracking.
|
||||
:param min_version: The minimum major version of a given API, intended to
|
||||
be used as the lower bound of a range with
|
||||
max_version. Mutually exclusive with version.
|
||||
If min_version is given with no max_version it is as
|
||||
if max version is 'latest'. (optional)
|
||||
:param max_version: The maximum major version of a given API, intended to
|
||||
be used as the upper bound of a range with min_version.
|
||||
Mutually exclusive with version. (optional)
|
||||
:param default_microversion: The default microversion value to send
|
||||
with API requests. While microversions are
|
||||
a per-request feature, a user may know they
|
||||
want to default to sending a specific value.
|
||||
(optional)
|
||||
"""
|
||||
|
||||
client_name = None
|
||||
client_version = None
|
||||
|
||||
@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, allow={},
|
||||
additional_headers=None, client_name=None,
|
||||
client_version=None, allow_version_hack=None,
|
||||
global_request_id=None,
|
||||
min_version=None, max_version=None,
|
||||
default_microversion=None):
|
||||
if version and (min_version or max_version):
|
||||
raise TypeError(
|
||||
"version is mutually exclusive with min_version and"
|
||||
" max_version")
|
||||
# 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
|
||||
self.allow = allow
|
||||
self.additional_headers = additional_headers or {}
|
||||
self.allow_version_hack = allow_version_hack
|
||||
self.min_version = min_version
|
||||
self.max_version = max_version
|
||||
self.default_microversion = default_microversion
|
||||
|
||||
self.global_request_id = global_request_id
|
||||
|
||||
if client_name:
|
||||
self.client_name = client_name
|
||||
if client_version:
|
||||
self.client_version = client_version
|
||||
|
||||
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)
|
||||
if self.min_version:
|
||||
kwargs.setdefault('min_version', self.min_version)
|
||||
if self.max_version:
|
||||
kwargs.setdefault('max_version', self.max_version)
|
||||
if self.allow_version_hack is not None:
|
||||
kwargs.setdefault('allow_version_hack', self.allow_version_hack)
|
||||
|
||||
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)
|
||||
if self.allow:
|
||||
kwargs.setdefault('allow', self.allow)
|
||||
if self.default_microversion is not None:
|
||||
kwargs.setdefault('microversion', self.default_microversion)
|
||||
|
||||
if isinstance(self.session, (session.Session, Adapter)):
|
||||
# these things are unsupported by keystoneclient's session so be
|
||||
# careful with them until everyone has transitioned to ksa.
|
||||
# Allowing adapter allows adapter nesting that auth_token does.
|
||||
if self.client_name:
|
||||
kwargs.setdefault('client_name', self.client_name)
|
||||
if self.client_version:
|
||||
kwargs.setdefault('client_version', self.client_version)
|
||||
|
||||
else:
|
||||
warnings.warn('Using keystoneclient sessions has been deprecated. '
|
||||
'Please update your software to use keystoneauth1.')
|
||||
|
||||
for k, v in self.additional_headers.items():
|
||||
kwargs.setdefault('headers', {}).setdefault(k, v)
|
||||
|
||||
if self.global_request_id is not None:
|
||||
kwargs.setdefault('headers', {}).setdefault(
|
||||
"X-OpenStack-Request-ID", self.global_request_id)
|
||||
|
||||
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 get_endpoint_data(self, auth=None):
|
||||
"""Get the endpoint data for this Adapter's endpoint.
|
||||
|
||||
: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.
|
||||
:raises TypeError: If arguments are invalid
|
||||
|
||||
:returns: Endpoint data if available or None.
|
||||
:rtype: keystoneauth1.discover.EndpointData
|
||||
"""
|
||||
kwargs = {}
|
||||
self._set_endpoint_filter_kwargs(kwargs)
|
||||
if self.endpoint_override:
|
||||
kwargs['endpoint_override'] = self.endpoint_override
|
||||
|
||||
return self.session.get_endpoint_data(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)
|
||||
|
||||
# TODO(efried): Move this to loading.adapter.Adapter
|
||||
@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='<name>',
|
||||
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='<name>',
|
||||
default=os.environ.get('OS_SERVICE_NAME', None),
|
||||
help='Service name to request from the catalog')
|
||||
|
||||
adapter_group.add_argument(
|
||||
'--os-interface',
|
||||
metavar='<name>',
|
||||
default=os.environ.get('OS_INTERFACE', 'public'),
|
||||
help='API Interface to use [public, internal, admin]')
|
||||
|
||||
adapter_group.add_argument(
|
||||
'--os-region-name',
|
||||
metavar='<name>',
|
||||
default=os.environ.get('OS_REGION_NAME', None),
|
||||
help='Region of the cloud to use')
|
||||
|
||||
adapter_group.add_argument(
|
||||
'--os-endpoint-override',
|
||||
metavar='<name>',
|
||||
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='<name>',
|
||||
default=os.environ.get('OS_API_VERSION', None),
|
||||
help='Which version of the service API to use')
|
||||
|
||||
# TODO(efried): Move this to loading.adapter.Adapter
|
||||
@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='<name>',
|
||||
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='<name>',
|
||||
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='<name>',
|
||||
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='<name>',
|
||||
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='<name>',
|
||||
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
|
||||
|
||||
|
||||
# TODO(efried): Deprecate this in favor of
|
||||
# loading.adapter.register_argparse_arguments
|
||||
def register_adapter_argparse_arguments(*args, **kwargs):
|
||||
return Adapter.register_argparse_arguments(*args, **kwargs)
|
||||
|
||||
|
||||
# TODO(efried): Deprecate this in favor of
|
||||
# loading.adapter.register_service_argparse_arguments
|
||||
def register_service_adapter_argparse_arguments(*args, **kwargs):
|
||||
return Adapter.register_service_argparse_arguments(*args, **kwargs)
|
File diff suppressed because it is too large
Load Diff
@ -1,23 +0,0 @@
|
||||
# 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.oidc import * # noqa
|
||||
from keystoneauth1.exceptions.response import * # noqa
|
||||
from keystoneauth1.exceptions.service_providers import * # noqa
|
@ -1,17 +0,0 @@
|
||||
# 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."
|
@ -1,93 +0,0 @@
|
||||
# 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)
|
@ -1,24 +0,0 @@
|
||||
# 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 = "ClientException"
|
||||
|
||||
def __init__(self, message=None):
|
||||
self.message = message or self.message
|
||||
super(ClientException, self).__init__(self.message)
|
@ -1,30 +0,0 @@
|
||||
# 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."
|
@ -1,51 +0,0 @@
|
||||
# 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
|
@ -1,25 +0,0 @@
|
||||
# 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."
|
@ -1,428 +0,0 @@
|
||||
# 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
|
||||
|
||||
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 vars(sys.modules[__name__]).items()
|
||||
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
||||
)
|
||||
|
||||
|
||||
def from_response(response, method, url):
|
||||
"""Return 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)
|
@ -1,44 +0,0 @@
|
||||
# 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 auth_plugins
|
||||
|
||||
__all__ = (
|
||||
'InvalidDiscoveryEndpoint', 'InvalidOidcDiscoveryDocument',
|
||||
'OidcAccessTokenEndpointNotFound', 'OidcAuthorizationEndpointNotFound',
|
||||
'OidcGrantTypeMissmatch', 'OidcPluginNotSupported',
|
||||
)
|
||||
|
||||
|
||||
class InvalidDiscoveryEndpoint(auth_plugins.AuthPluginException):
|
||||
message = "OpenID Connect Discovery Document endpoint not set."""
|
||||
|
||||
|
||||
class InvalidOidcDiscoveryDocument(auth_plugins.AuthPluginException):
|
||||
message = "OpenID Connect Discovery Document is not valid JSON."""
|
||||
|
||||
|
||||
class OidcAccessTokenEndpointNotFound(auth_plugins.AuthPluginException):
|
||||
message = "OpenID Connect access token endpoint not provided."
|
||||
|
||||
|
||||
class OidcAuthorizationEndpointNotFound(auth_plugins.AuthPluginException):
|
||||
message = "OpenID Connect authorization endpoint not provided."
|
||||
|
||||
|
||||
class OidcGrantTypeMissmatch(auth_plugins.AuthPluginException):
|
||||
message = "Missmatch between OpenID Connect plugin and grant_type argument"
|
||||
|
||||
|
||||
class OidcPluginNotSupported(auth_plugins.AuthPluginException):
|
||||
message = "OpenID Connect grant type not supported by provider."
|
@ -1,25 +0,0 @@
|
||||
# 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
|
@ -1,24 +0,0 @@
|
||||
# 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)
|
@ -1,20 +0,0 @@
|
||||
# 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
|
@ -1,22 +0,0 @@
|
||||
# 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
|
||||
|
||||
_V3_SAML2_AVAILABLE = v3._SAML2_AVAILABLE
|
||||
_V3_ADFS_AVAILABLE = v3._ADFS_AVAILABLE
|
||||
|
||||
V3Saml2Password = v3.Saml2Password
|
||||
V3ADFSPassword = v3.ADFSPassword
|
||||
|
||||
|
||||
__all__ = ('V3Saml2Password', 'V3ADFSPassword')
|
@ -1,66 +0,0 @@
|
||||
# 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
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return _saml2._V3_SAML2_AVAILABLE
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return _saml2._V3_ADFS_AVAILABLE
|
||||
|
||||
def get_options(self):
|
||||
options = super(ADFSPassword, self).get_options()
|
||||
|
||||
options.extend([
|
||||
loading.Opt('identity-provider-url',
|
||||
help=('An Identity Provider URL, where the SAML '
|
||||
'authentication request will be sent.')),
|
||||
loading.Opt('service-provider-endpoint',
|
||||
help="Service Provider's Endpoint"),
|
||||
loading.Opt('service-provider-entity-id',
|
||||
help="Service Provider's SAML Entity ID"),
|
||||
loading.Opt('username', help='Username'),
|
||||
loading.Opt('password', secret=True, help='Password')
|
||||
])
|
||||
|
||||
return options
|
@ -1,23 +0,0 @@
|
||||
# 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 base
|
||||
from keystoneauth1.extras._saml2.v3 import saml2
|
||||
|
||||
_SAML2_AVAILABLE = base.etree is not None and saml2.etree is not None
|
||||
_ADFS_AVAILABLE = base.etree is not None and adfs.etree is not None
|
||||
|
||||
Saml2Password = saml2.Password
|
||||
ADFSPassword = adfs.Password
|
||||
|
||||
__all__ = ('Saml2Password', 'ADFSPassword')
|
@ -1,431 +0,0 @@
|
||||
# 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
|
||||
|
||||
try:
|
||||
from lxml import etree
|
||||
except ImportError:
|
||||
etree = None
|
||||
|
||||
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, service_provider_entity_id=None, **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 service_provider_entity_id: Service Provider SAML Entity ID
|
||||
:type service_provider_entity_id: 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, **kwargs)
|
||||
|
||||
self.service_provider_endpoint = service_provider_endpoint
|
||||
self.service_provider_entity_id = service_provider_entity_id
|
||||
|
||||
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_entity_id or
|
||||
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)
|
@ -1,98 +0,0 @@
|
||||
# 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.
|
||||
|
||||
try:
|
||||
from lxml import etree
|
||||
except ImportError:
|
||||
etree = None
|
||||
|
||||
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 auhentication 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)
|
@ -1,301 +0,0 @@
|
||||
# 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
|
||||
|
||||
try:
|
||||
from lxml import etree
|
||||
except ImportError:
|
||||
etree = None
|
||||
|
||||
import requests
|
||||
import requests.auth
|
||||
|
||||
from keystoneauth1 import access
|
||||
from keystoneauth1 import exceptions
|
||||
from keystoneauth1.identity import v3
|
||||
|
||||
_PAOS_NAMESPACE = 'urn:liberty:paos:2003-08'
|
||||
_ECP_NAMESPACE = 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'
|
||||
_PAOS_HEADER = 'application/vnd.paos+xml'
|
||||
|
||||
_PAOS_VER = 'ver="%s";"%s"' % (_PAOS_NAMESPACE, _ECP_NAMESPACE)
|
||||
|
||||
_XML_NAMESPACES = {
|
||||
'ecp': _ECP_NAMESPACE,
|
||||
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
|
||||
'paos': _PAOS_NAMESPACE,
|
||||
}
|
||||
|
||||
_XBASE = '/S:Envelope/S:Header/'
|
||||
|
||||
_XPATH_SP_RELAY_STATE = '//ecp:RelayState'
|
||||
_XPATH_SP_CONSUMER_URL = _XBASE + 'paos:Request/@responseConsumerURL'
|
||||
_XPATH_IDP_CONSUMER_URL = _XBASE + 'ecp:Response/@AssertionConsumerServiceURL'
|
||||
|
||||
_SOAP_FAULT = """
|
||||
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<S:Body>
|
||||
<S:Fault>
|
||||
<faultcode>S:Server</faultcode>
|
||||
<faultstring>responseConsumerURL from SP and
|
||||
assertionConsumerServiceURL from IdP do not match
|
||||
</faultstring>
|
||||
</S:Fault>
|
||||
</S:Body>
|
||||
</S:Envelope>
|
||||
"""
|
||||
|
||||
|
||||
class SamlException(Exception):
|
||||
"""Base SAML plugin exception."""
|
||||
|
||||
|
||||
class InvalidResponse(SamlException):
|
||||
"""Invalid Response from SAML authentication."""
|
||||
|
||||
|
||||
class ConsumerMismatch(SamlException):
|
||||
"""The SP and IDP consumers do not match."""
|
||||
|
||||
|
||||
def _response_xml(response, name):
|
||||
try:
|
||||
return etree.XML(response.content)
|
||||
except etree.XMLSyntaxError as e:
|
||||
msg = 'SAML2: Error parsing XML returned from %s: %s' % (name, e)
|
||||
raise InvalidResponse(msg)
|
||||
|
||||
|
||||
def _str_from_xml(xml, path):
|
||||
l = xml.xpath(path, namespaces=_XML_NAMESPACES)
|
||||
if len(l) != 1:
|
||||
raise IndexError('%s should provide a single element list' % path)
|
||||
return l[0]
|
||||
|
||||
|
||||
class _SamlAuth(requests.auth.AuthBase):
|
||||
"""A generic SAML ECP plugin for requests.
|
||||
|
||||
This is a multi-step process including multiple HTTP requests.
|
||||
Authentication consists of:
|
||||
|
||||
* HTTP GET request to the Service Provider.
|
||||
|
||||
It's crucial to include HTTP headers indicating we are expecting SOAP
|
||||
message in return. Service Provider should respond with a SOAP
|
||||
message.
|
||||
|
||||
* 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.
|
||||
|
||||
* 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.
|
||||
"""
|
||||
|
||||
def __init__(self, identity_provider_url, requests_auth):
|
||||
super(_SamlAuth, self).__init__()
|
||||
self.identity_provider_url = identity_provider_url
|
||||
self.requests_auth = requests_auth
|
||||
|
||||
def __call__(self, request):
|
||||
try:
|
||||
accept = request.headers['Accept']
|
||||
except KeyError:
|
||||
request.headers['Accept'] = _PAOS_HEADER
|
||||
else:
|
||||
request.headers['Accept'] = ','.join([accept, _PAOS_HEADER])
|
||||
|
||||
request.headers['PAOS'] = _PAOS_VER
|
||||
request.register_hook('response', self._handle_response)
|
||||
return request
|
||||
|
||||
def _handle_response(self, response, **kwargs):
|
||||
if (response.status_code == 200 and
|
||||
response.headers.get('Content-Type') == _PAOS_HEADER):
|
||||
response = self._ecp_retry(response, **kwargs)
|
||||
|
||||
return response
|
||||
|
||||
def _ecp_retry(self, sp_response, **kwargs):
|
||||
history = [sp_response]
|
||||
|
||||
def send(*send_args, **send_kwargs):
|
||||
req = requests.Request(*send_args, **send_kwargs)
|
||||
return sp_response.connection.send(req.prepare(), **kwargs)
|
||||
|
||||
authn_request = _response_xml(sp_response, 'Service Provider')
|
||||
relay_state = _str_from_xml(authn_request, _XPATH_SP_RELAY_STATE)
|
||||
sp_consumer_url = _str_from_xml(authn_request, _XPATH_SP_CONSUMER_URL)
|
||||
|
||||
authn_request.remove(authn_request[0])
|
||||
|
||||
idp_response = send('POST',
|
||||
self.identity_provider_url,
|
||||
headers={'Content-type': 'text/xml'},
|
||||
data=etree.tostring(authn_request),
|
||||
auth=self.requests_auth)
|
||||
history.append(idp_response)
|
||||
|
||||
authn_response = _response_xml(idp_response, 'Identity Provider')
|
||||
idp_consumer_url = _str_from_xml(authn_response,
|
||||
_XPATH_IDP_CONSUMER_URL)
|
||||
|
||||
if sp_consumer_url != idp_consumer_url:
|
||||
# send fault message to the SP, discard the response
|
||||
send('POST',
|
||||
sp_consumer_url,
|
||||
data=_SOAP_FAULT,
|
||||
headers={'Content-Type': _PAOS_HEADER})
|
||||
|
||||
# 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': sp_response.request.url,
|
||||
'sp_consumer_url': sp_consumer_url,
|
||||
'identity_provider': self.identity_provider_url,
|
||||
'idp_consumer_url': idp_consumer_url
|
||||
}
|
||||
|
||||
raise ConsumerMismatch(msg)
|
||||
|
||||
authn_response[0][0] = relay_state
|
||||
|
||||
# idp_consumer_url is the URL on the SP that handles the ECP body
|
||||
# returned and creates an authenticated session.
|
||||
final_resp = send('POST',
|
||||
idp_consumer_url,
|
||||
headers={'Content-Type': _PAOS_HEADER},
|
||||
cookies=idp_response.cookies,
|
||||
data=etree.tostring(authn_response))
|
||||
|
||||
history.append(final_resp)
|
||||
|
||||
# the SP should then redirect us back to the original URL to retry the
|
||||
# original request.
|
||||
if final_resp.status_code in (requests.codes.found,
|
||||
requests.codes.other):
|
||||
|
||||
# Consume content and release the original connection
|
||||
# to allow our new request to reuse the same one.
|
||||
sp_response.content
|
||||
sp_response.raw.release_conn()
|
||||
|
||||
req = sp_response.request.copy()
|
||||
req.url = final_resp.headers['location']
|
||||
req.prepare_cookies(final_resp.cookies)
|
||||
|
||||
final_resp = sp_response.connection.send(req, **kwargs)
|
||||
history.append(final_resp)
|
||||
|
||||
final_resp.history.extend(history)
|
||||
return final_resp
|
||||
|
||||
|
||||
class _FederatedSaml(v3.FederationBaseAuth):
|
||||
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
identity_provider_url, **kwargs):
|
||||
super(_FederatedSaml, self).__init__(auth_url,
|
||||
identity_provider,
|
||||
protocol,
|
||||
**kwargs)
|
||||
self.identity_provider_url = identity_provider_url
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_requests_auth(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_unscoped_auth_ref(self, session, **kwargs):
|
||||
method = self.get_requests_auth()
|
||||
auth = _SamlAuth(self.identity_provider_url, method)
|
||||
|
||||
try:
|
||||
resp = session.get(self.federated_token_url,
|
||||
requests_auth=auth,
|
||||
authenticated=False)
|
||||
except SamlException as e:
|
||||
raise exceptions.AuthorizationFailure(str(e))
|
||||
|
||||
return access.create(resp=resp)
|
||||
|
||||
|
||||
class Password(_FederatedSaml):
|
||||
r"""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
|
||||
<https://wiki.shibboleth.net/confluence/display/CONCEPT/ECP>`_ on ECP.
|
||||
|
||||
Reference the `SAML2 ECP specification <https://www.oasis-open.org/\
|
||||
committees/download.php/49979/saml-ecp-v2.0-wd09.pdf>`_.
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
identity_provider_url, username, password, **kwargs):
|
||||
super(Password, self).__init__(auth_url,
|
||||
identity_provider,
|
||||
protocol,
|
||||
identity_provider_url,
|
||||
**kwargs)
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def get_requests_auth(self):
|
||||
return requests.auth.HTTPBasicAuth(self.username, self.password)
|
@ -1,89 +0,0 @@
|
||||
# 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 keystoneauth1[kerberos]
|
||||
"""
|
||||
|
||||
try:
|
||||
import requests_kerberos
|
||||
except ImportError:
|
||||
requests_kerberos = None
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _dependency_check():
|
||||
if requests_kerberos is None:
|
||||
raise ImportError("""
|
||||
Using the kerberos authentication plugin requires installation of additional
|
||||
packages. These can be installed with::
|
||||
|
||||
$ pip install keystoneauth1[kerberos]
|
||||
""")
|
||||
|
||||
|
||||
class KerberosMethod(v3.AuthMethod):
|
||||
|
||||
_method_parameters = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
_dependency_check()
|
||||
super(KerberosMethod, self).__init__(*args, **kwargs)
|
||||
|
||||
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 __init__(self, auth_url, identity_provider, protocol, **kwargs):
|
||||
_dependency_check()
|
||||
|
||||
super(MappedKerberos, self).__init__(auth_url, identity_provider,
|
||||
protocol, **kwargs)
|
||||
|
||||
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)
|
@ -1,36 +0,0 @@
|
||||
# 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 kerberos
|
||||
from keystoneauth1 import loading
|
||||
|
||||
|
||||
class Kerberos(loading.BaseV3Loader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return kerberos.Kerberos
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return kerberos.requests_kerberos is not None
|
||||
|
||||
|
||||
class MappedKerberos(loading.BaseFederationLoader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return kerberos.MappedKerberos
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return kerberos.requests_kerberos is not None
|
@ -1,19 +0,0 @@
|
||||
# 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.oauth1 import v3
|
||||
|
||||
__all__ = ('V3OAuth1Method', 'V3OAuth1')
|
||||
|
||||
|
||||
V3OAuth1Method = v3.OAuth1Method
|
||||
V3OAuth1 = v3.OAuth1
|
@ -1,47 +0,0 @@
|
||||
# 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.oauth1 import v3
|
||||
from keystoneauth1 import loading
|
||||
|
||||
|
||||
# NOTE(jamielennox): This is not a BaseV3Loader because we don't want to
|
||||
# include the scoping options like project-id in the option list
|
||||
class V3OAuth1(loading.BaseIdentityLoader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return v3.OAuth1
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return v3.oauth1 is not None
|
||||
|
||||
def get_options(self):
|
||||
options = super(V3OAuth1, self).get_options()
|
||||
|
||||
options.extend([
|
||||
loading.Opt('consumer-key',
|
||||
required=True,
|
||||
help='OAuth Consumer ID/Key'),
|
||||
loading.Opt('consumer-secret',
|
||||
required=True,
|
||||
help='OAuth Consumer Secret'),
|
||||
loading.Opt('access-key',
|
||||
required=True,
|
||||
help='OAuth Access Key'),
|
||||
loading.Opt('access-secret',
|
||||
required=True,
|
||||
help='OAuth Access Secret'),
|
||||
])
|
||||
|
||||
return options
|
@ -1,80 +0,0 @@
|
||||
# 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.
|
||||
|
||||
"""Oauth authentication plugins.
|
||||
|
||||
.. warning::
|
||||
This module requires installation of an extra package (`oauthlib`)
|
||||
not installed by default. Without the extra package an import error will
|
||||
occur. The extra package can be installed using::
|
||||
|
||||
$ pip install keystoneauth['oauth1']
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
try:
|
||||
from oauthlib import oauth1
|
||||
except ImportError:
|
||||
oauth1 = None
|
||||
|
||||
from keystoneauth1.identity import v3
|
||||
|
||||
__all__ = ('OAuth1Method', 'OAuth1')
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OAuth1Method(v3.AuthMethod):
|
||||
"""OAuth based authentication method.
|
||||
|
||||
:param string consumer_key: Consumer key.
|
||||
:param string consumer_secret: Consumer secret.
|
||||
:param string access_key: Access token key.
|
||||
:param string access_secret: Access token secret.
|
||||
"""
|
||||
|
||||
_method_parameters = ['consumer_key', 'consumer_secret',
|
||||
'access_key', 'access_secret']
|
||||
|
||||
def get_auth_data(self, session, auth, headers, **kwargs):
|
||||
# Add the oauth specific content into the headers
|
||||
oauth_client = oauth1.Client(self.consumer_key,
|
||||
client_secret=self.consumer_secret,
|
||||
resource_owner_key=self.access_key,
|
||||
resource_owner_secret=self.access_secret,
|
||||
signature_method=oauth1.SIGNATURE_HMAC)
|
||||
|
||||
o_url, o_headers, o_body = oauth_client.sign(auth.token_url,
|
||||
http_method='POST')
|
||||
headers.update(o_headers)
|
||||
|
||||
return 'oauth1', {}
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
return dict(('oauth1_%s' % p, getattr(self, p))
|
||||
for p in self._method_parameters)
|
||||
|
||||
|
||||
class OAuth1(v3.AuthConstructor):
|
||||
|
||||
_auth_method_class = OAuth1Method
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OAuth1, self).__init__(*args, **kwargs)
|
||||
|
||||
if self.has_scope_parameters:
|
||||
LOG.warning('Scoping parameters such as a project were provided '
|
||||
'to the OAuth1 plugin. Because OAuth1 access is '
|
||||
'always scoped to a project these will be ignored by '
|
||||
'the identity server')
|
@ -1,41 +0,0 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Produce keystone compliant structures for use in testing.
|
||||
|
||||
They are 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 (keystoneauth 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',
|
||||
'VersionDiscovery',
|
||||
)
|
@ -1,373 +0,0 @@
|
||||
# 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',
|
||||
'VersionDiscovery',
|
||||
)
|
||||
|
||||
_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 VersionDiscovery(DiscoveryBase):
|
||||
"""A Version element for non-keystone services without microversions.
|
||||
|
||||
Provides some default values and helper methods for creating a microversion
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, href, id, **kwargs):
|
||||
super(VersionDiscovery, self).__init__(id, **kwargs)
|
||||
|
||||
self.add_link(href)
|
||||
|
||||
|
||||
class MicroversionDiscovery(DiscoveryBase):
|
||||
"""A Version element that has microversions.
|
||||
|
||||
Provides some default values and helper methods for creating a microversion
|
||||
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.
|
||||
:param string min_version: The minimum supported microversion. (optional)
|
||||
:param string max_version: The maximum supported microversion. (optional)
|
||||
"""
|
||||
|
||||
@positional()
|
||||
def __init__(self, href, id, min_version='', max_version='', **kwargs):
|
||||
super(MicroversionDiscovery, self).__init__(id, **kwargs)
|
||||
|
||||
self.add_link(href)
|
||||
|
||||
self.min_version = min_version
|
||||
self.max_version = max_version
|
||||
|
||||
@property
|
||||
def min_version(self):
|
||||
return self.get('min_version')
|
||||
|
||||
@min_version.setter
|
||||
def min_version(self, value):
|
||||
self['min_version'] = value
|
||||
|
||||
@property
|
||||
def max_version(self):
|
||||
return self.get('max_version')
|
||||
|
||||
@max_version.setter
|
||||
def max_version(self, value):
|
||||
self['max_version'] = value
|
||||
|
||||
|
||||
class NovaMicroversionDiscovery(DiscoveryBase):
|
||||
"""A Version element with nova-style microversions.
|
||||
|
||||
Provides some default values and helper methods for creating a microversion
|
||||
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.
|
||||
:param string min_version: The minimum microversion supported. (optional)
|
||||
:param string version: The maximum microversion supported. (optional)
|
||||
"""
|
||||
|
||||
@positional()
|
||||
def __init__(self, href, id, min_version=None, version=None, **kwargs):
|
||||
super(NovaMicroversionDiscovery, self).__init__(id, **kwargs)
|
||||
|
||||
self.add_link(href)
|
||||
|
||||
self.min_version = min_version
|
||||
self.version = version
|
||||
|
||||
@property
|
||||
def min_version(self):
|
||||
return self.get('min_version')
|
||||
|
||||
@min_version.setter
|
||||
def min_version(self, value):
|
||||
if value:
|
||||
self['min_version'] = value
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self.get('version')
|
||||
|
||||
@version.setter
|
||||
def version(self, value):
|
||||
if value:
|
||||
self['version'] = value
|
||||
|
||||
|
||||
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 = 'https://developer.openstack.org/api-ref/identity/v2/'
|
||||
|
||||
@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
|
||||
|
||||
def add_microversion(self, href, id, **kwargs):
|
||||
"""Add a microversion version to the list.
|
||||
|
||||
The parameters are the same as MicroversionDiscovery.
|
||||
"""
|
||||
obj = MicroversionDiscovery(href=href, id=id, **kwargs)
|
||||
self.add_version(obj)
|
||||
return obj
|
||||
|
||||
def add_nova_microversion(self, href, id, **kwargs):
|
||||
"""Add a nova microversion version to the list.
|
||||
|
||||
The parameters are the same as NovaMicroversionDiscovery.
|
||||
"""
|
||||
obj = NovaMicroversionDiscovery(href=href, id=id, **kwargs)
|
||||
self.add_version(obj)
|
||||
return obj
|
@ -1,20 +0,0 @@
|
||||
# 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.
|
||||
"""
|
@ -1,60 +0,0 @@
|
||||
# Copyright (c) 2016 Hewlett-Packard Enterprise 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.
|
||||
"""Custom hooks for betamax and keystoneauth.
|
||||
|
||||
Module providing a set of hooks specially designed for
|
||||
interacting with clouds and keystone authentication.
|
||||
|
||||
:author: Yolanda Robla
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def mask_fixture_values(nested, prev_key):
|
||||
for key, value in nested.items():
|
||||
if isinstance(value, dict):
|
||||
mask_fixture_values(value, key)
|
||||
else:
|
||||
if key in ('tenantName', 'username'):
|
||||
nested[key] = 'dummy'
|
||||
elif prev_key in ('user', 'project', 'tenant') and key == 'name':
|
||||
nested[key] = 'dummy'
|
||||
elif prev_key == 'domain' and key == 'id':
|
||||
nested[key] = 'dummy'
|
||||
elif key == 'password':
|
||||
nested[key] = '********'
|
||||
elif prev_key == 'token' and key in ('expires', 'expires_at'):
|
||||
nested[key] = '9999-12-31T23:59:59Z'
|
||||
|
||||
|
||||
def pre_record_hook(interaction, cassette):
|
||||
"""Hook to mask saved data.
|
||||
|
||||
This hook will be triggered before saving the interaction, and
|
||||
will perform two tasks:
|
||||
- mask user, project and password in the saved data
|
||||
- set token expiration time to an inifinite time.
|
||||
"""
|
||||
request_body = interaction.data['request']['body']
|
||||
if request_body.get('string'):
|
||||
parsed_content = json.loads(request_body['string'])
|
||||
mask_fixture_values(parsed_content, None)
|
||||
request_body['string'] = json.dumps(parsed_content)
|
||||
|
||||
response_body = interaction.data['response']['body']
|
||||
if response_body.get('string'):
|
||||
parsed_content = json.loads(response_body['string'])
|
||||
mask_fixture_values(parsed_content, None)
|
||||
response_body['string'] = json.dumps(parsed_content)
|
@ -1,136 +0,0 @@
|
||||
# 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.fixture import hooks
|
||||
from keystoneauth1.fixture import serializer as yaml_serializer
|
||||
from keystoneauth1 import session
|
||||
|
||||
|
||||
class BetamaxFixture(fixtures.Fixture):
|
||||
|
||||
def __init__(self, cassette_name, cassette_library_dir=None,
|
||||
serializer=None, record=False,
|
||||
pre_record_hook=hooks.pre_record_hook,
|
||||
serializer_name=None, request_matchers=None):
|
||||
"""Configure Betamax for the test suite.
|
||||
|
||||
:param str cassette_name:
|
||||
This is simply the name of the cassette without any file extension
|
||||
or containing directory. For example, to generate
|
||||
``keystoneauth1/tests/unit/data/example.yaml``, one would pass
|
||||
only ``example``.
|
||||
:param str cassette_library_dir:
|
||||
This is the directory that will contain all cassette files. In
|
||||
``keystoneauth1/tests/unit/data/example.yaml`` you would pass
|
||||
``keystoneauth1/tests/unit/data/``.
|
||||
:param serializer:
|
||||
A class that implements the Serializer API in Betamax. See also:
|
||||
https://betamax.readthedocs.io/en/latest/serializers.html
|
||||
:param record:
|
||||
The Betamax record mode to use. If ``False`` (the default), then
|
||||
Betamax will not record anything. For more information about
|
||||
record modes, see:
|
||||
https://betamax.readthedocs.io/en/latest/record_modes.html
|
||||
:param callable pre_record_hook:
|
||||
Function or callable to use to perform some handling of the
|
||||
request or response data prior to saving it to disk.
|
||||
:param str serializer_name:
|
||||
The name of a serializer already registered with Betamax to use
|
||||
to handle cassettes. For example, if you want to use the default
|
||||
Betamax serializer, you would pass ``'json'`` to this parameter.
|
||||
:param list request_matchers:
|
||||
The list of request matcher names to use with Betamax. Betamax's
|
||||
default list is used if none are specified. See also:
|
||||
https://betamax.readthedocs.io/en/latest/matchers.html
|
||||
"""
|
||||
self.cassette_library_dir = cassette_library_dir
|
||||
self.record = record
|
||||
self.cassette_name = cassette_name
|
||||
if not (serializer or serializer_name):
|
||||
serializer = yaml_serializer.YamlJsonSerializer
|
||||
serializer_name = serializer.name
|
||||
if serializer:
|
||||
betamax.Betamax.register_serializer(serializer)
|
||||
self.serializer = serializer
|
||||
self._serializer_name = serializer_name
|
||||
self.pre_record_hook = pre_record_hook
|
||||
self.use_cassette_kwargs = {}
|
||||
if request_matchers is not None:
|
||||
self.use_cassette_kwargs['match_requests_on'] = request_matchers
|
||||
|
||||
@property
|
||||
def serializer_name(self):
|
||||
"""Determine the name of the selected serializer.
|
||||
|
||||
If a class was specified, use the name attribute to generate this,
|
||||
otherwise, use the serializer_name parameter from ``__init__``.
|
||||
|
||||
:returns:
|
||||
Name of the serializer
|
||||
:rtype:
|
||||
str
|
||||
"""
|
||||
if self.serializer:
|
||||
return self.serializer.name
|
||||
return self._serializer_name
|
||||
|
||||
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())
|
||||
|
||||
with betamax.Betamax.configure() as config:
|
||||
config.before_record(callback=fixture.pre_record_hook)
|
||||
fixture.recorder = betamax.Betamax(
|
||||
session_obj, cassette_library_dir=fixture.cassette_library_dir)
|
||||
|
||||
record = 'none'
|
||||
serializer = None
|
||||
|
||||
if fixture.record in ['once', 'all', 'new_episodes']:
|
||||
record = fixture.record
|
||||
|
||||
serializer = fixture.serializer_name
|
||||
|
||||
fixture.recorder.use_cassette(fixture.cassette_name,
|
||||
serialize_with=serializer,
|
||||
record=record,
|
||||
**fixture.use_cassette_kwargs)
|
||||
|
||||
fixture.recorder.start()
|
||||
fixture.addCleanup(fixture.recorder.stop)
|
||||
return session_obj
|
@ -1,98 +0,0 @@
|
||||
# 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 serializer to emit YAML but with request body in nicely formatted JSON."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import betamax.serializers.base
|
||||
import six
|
||||
import yaml
|
||||
|
||||
|
||||
def _should_use_block(value):
|
||||
for c in u"\u000a\u000d\u001c\u001d\u001e\u0085\u2028\u2029":
|
||||
if c in value:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _represent_scalar(self, tag, value, style=None):
|
||||
if style is None:
|
||||
if _should_use_block(value):
|
||||
style = '|'
|
||||
else:
|
||||
style = self.default_style
|
||||
|
||||
node = yaml.representer.ScalarNode(tag, value, style=style)
|
||||
if self.alias_key is not None:
|
||||
self.represented_objects[self.alias_key] = node
|
||||
return node
|
||||
|
||||
|
||||
def _unicode_representer(dumper, uni):
|
||||
node = yaml.ScalarNode(tag=u'tag:yaml.org,2002:str', value=uni)
|
||||
return node
|
||||
|
||||
|
||||
def _indent_json(val):
|
||||
if not val:
|
||||
return ''
|
||||
|
||||
return json.dumps(
|
||||
json.loads(val), indent=2,
|
||||
separators=(',', ': '), sort_keys=False,
|
||||
default=six.text_type)
|
||||
|
||||
|
||||
def _is_json_body(interaction):
|
||||
content_type = interaction['headers'].get('Content-Type', [])
|
||||
return 'application/json' in content_type
|
||||
|
||||
|
||||
class YamlJsonSerializer(betamax.serializers.base.BaseSerializer):
|
||||
|
||||
name = "yamljson"
|
||||
|
||||
@staticmethod
|
||||
def generate_cassette_name(cassette_library_dir, cassette_name):
|
||||
return os.path.join(
|
||||
cassette_library_dir, "{name}.yaml".format(name=cassette_name))
|
||||
|
||||
def serialize(self, cassette_data):
|
||||
# Reserialize internal json with indentation
|
||||
for interaction in cassette_data['http_interactions']:
|
||||
for key in ('request', 'response'):
|
||||
if _is_json_body(interaction[key]):
|
||||
interaction[key]['body']['string'] = _indent_json(
|
||||
interaction[key]['body']['string'])
|
||||
|
||||
class MyDumper(yaml.Dumper):
|
||||
"""Specialized Dumper which does nice blocks and unicode."""
|
||||
|
||||
yaml.representer.BaseRepresenter.represent_scalar = _represent_scalar
|
||||
|
||||
MyDumper.add_representer(six.text_type, _unicode_representer)
|
||||
|
||||
return yaml.dump(
|
||||
cassette_data, Dumper=MyDumper, default_flow_style=False)
|
||||
|
||||
def deserialize(self, cassette_data):
|
||||
try:
|
||||
deserialized = yaml.safe_load(cassette_data)
|
||||
except yaml.error.YAMLError:
|
||||
deserialized = None
|
||||
|
||||
if deserialized is not None:
|
||||
return deserialized
|
||||
return {}
|
@ -1,247 +0,0 @@
|
||||
# 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
|
@ -1,466 +0,0 @@
|
||||
# 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,
|
||||
is_admin_project=None, project_is_domain=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,
|
||||
is_domain=project_is_domain)
|
||||
|
||||
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
|
||||
|
||||
if is_admin_project is not None:
|
||||
self.is_admin_project = is_admin_project
|
||||
|
||||
@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_is_domain(self):
|
||||
return self.root.get('is_domain')
|
||||
|
||||
@project_is_domain.setter
|
||||
def project_is_domain(self, value):
|
||||
self.root['is_domain'] = 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', [])]
|
||||
|
||||
@property
|
||||
def is_admin_project(self):
|
||||
return self.root.get('is_admin_project')
|
||||
|
||||
@is_admin_project.setter
|
||||
def is_admin_project(self, value):
|
||||
self.root['is_admin_project'] = value
|
||||
|
||||
@is_admin_project.deleter
|
||||
def is_admin_project(self):
|
||||
self.root.pop('is_admin_project', None)
|
||||
|
||||
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, is_domain=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
|
||||
|
||||
if is_domain is not None:
|
||||
self.project_is_domain = is_domain
|
||||
|
||||
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
|
@ -1,37 +0,0 @@
|
||||
# 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)
|
@ -1,69 +0,0 @@
|
||||
# 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
|
||||
"""See :class:`keystoneauth1.identity.v2.Password`"""
|
||||
|
||||
V2Token = v2.Token
|
||||
"""See :class:`keystoneauth1.identity.v2.Token`"""
|
||||
|
||||
V3Password = v3.Password
|
||||
"""See :class:`keystoneauth1.identity.v3.Password`"""
|
||||
|
||||
V3Token = v3.Token
|
||||
"""See :class:`keystoneauth1.identity.v3.Token`"""
|
||||
|
||||
Password = generic.Password
|
||||
"""See :class:`keystoneauth1.identity.generic.Password`"""
|
||||
|
||||
Token = generic.Token
|
||||
"""See :class:`keystoneauth1.identity.generic.Token`"""
|
||||
|
||||
V3OidcClientCredentials = oidc.OidcClientCredentials
|
||||
"""See :class:`keystoneauth1.identity.v3.oidc.OidcClientCredentials`"""
|
||||
|
||||
V3OidcPassword = oidc.OidcPassword
|
||||
"""See :class:`keystoneauth1.identity.v3.oidc.OidcPassword`"""
|
||||
|
||||
V3OidcAuthorizationCode = oidc.OidcAuthorizationCode
|
||||
"""See :class:`keystoneauth1.identity.v3.oidc.OidcAuthorizationCode`"""
|
||||
|
||||
V3OidcAccessToken = oidc.OidcAccessToken
|
||||
"""See :class:`keystoneauth1.identity.v3.oidc.OidcAccessToken`"""
|
||||
|
||||
V3TOTP = v3.TOTP
|
||||
"""See :class:`keystoneauth1.identity.v3.TOTP`"""
|
||||
|
||||
V3TokenlessAuth = v3.TokenlessAuth
|
||||
"""See :class:`keystoneauth1.identity.v3.TokenlessAuth`"""
|
||||
|
||||
__all__ = ('BaseIdentityPlugin',
|
||||
'Password',
|
||||
'Token',
|
||||
'V2Password',
|
||||
'V2Token',
|
||||
'V3Password',
|
||||
'V3Token',
|
||||
'V3OidcPassword',
|
||||
'V3OidcAuthorizationCode',
|
||||
'V3OidcAccessToken',
|
||||
'V3TOTP',
|
||||
'V3TokenlessAuth')
|
@ -1,48 +0,0 @@
|
||||
# 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: keystoneauth1.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
|
@ -1,507 +0,0 @@
|
||||
# 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._discovery_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: keystoneauth1.session.Session
|
||||
|
||||
:raises keystoneauth1.exceptions.response.InvalidResponse:
|
||||
The response returned wasn't appropriate.
|
||||
:raises keystoneauth1.exceptions.http.HttpError:
|
||||
An error from an invalid HTTP response.
|
||||
|
||||
:returns: Token access information.
|
||||
:rtype: :class:`keystoneauth1.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: keystoneauth1.session.Session
|
||||
|
||||
:raises keystoneauth1.exceptions.http.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: keystoneauth1.session.Session
|
||||
|
||||
:raises keystoneauth1.exceptions.http.HttpError: An error from an
|
||||
invalid HTTP response.
|
||||
|
||||
:returns: Valid AccessInfo
|
||||
:rtype: :class:`keystoneauth1.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_data(self, session, service_type=None, interface=None,
|
||||
region_name=None, service_name=None, allow={},
|
||||
allow_version_hack=True, discover_versions=True,
|
||||
skip_discovery=False, min_version=None,
|
||||
max_version=None, endpoint_override=None, **kwargs):
|
||||
"""Return a valid endpoint data for a service.
|
||||
|
||||
If a valid token is not present then a new one will be fetched using
|
||||
the session and kwargs.
|
||||
|
||||
version, min_version and max_version can all be given either as a
|
||||
string or a tuple.
|
||||
|
||||
Valid interface types: `public` or `publicURL`,
|
||||
`internal` or `internalURL`,
|
||||
`admin` or 'adminURL`
|
||||
|
||||
:param session: A session object that can be used for communication.
|
||||
:type session: keystoneauth1.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 interface: Type of endpoint. Can be a single value or a list
|
||||
of values. If it's a list of values, they will be
|
||||
looked for in order of preference. Can also be
|
||||
`keystoneauth1.plugin.AUTH_INTERFACE` to indicate
|
||||
that the auth_url should be used instead of the
|
||||
value in the catalog. (optional, 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 dict allow: Extra filters to pass when discovering API
|
||||
versions. (optional)
|
||||
:param bool allow_version_hack: Allow keystoneauth to hack up catalog
|
||||
URLS to support older schemes.
|
||||
(optional, default True)
|
||||
:param bool discover_versions: Whether to get version metadata from
|
||||
the version discovery document even
|
||||
if it's not neccessary to fulfill the
|
||||
major version request. (optional,
|
||||
defaults to True)
|
||||
:param bool skip_discovery: Whether to skip version discovery even
|
||||
if a version has been given. This is useful
|
||||
if endpoint_override or similar has been
|
||||
given and grabbing additional information
|
||||
about the endpoint is not useful.
|
||||
:param min_version: The minimum version that is acceptable. Mutually
|
||||
exclusive with version. If min_version is given
|
||||
with no max_version it is as if max version is
|
||||
'latest'. (optional)
|
||||
:param max_version: The maximum version that is acceptable. Mutually
|
||||
exclusive with version. If min_version is given
|
||||
with no max_version it is as if max version is
|
||||
'latest'. (optional)
|
||||
:param str endpoint_override: URL to use instead of looking in the
|
||||
catalog. Catalog lookup will be skipped,
|
||||
but version discovery will be run.
|
||||
Sets allow_version_hack to False
|
||||
(optional)
|
||||
:param kwargs: Ignored.
|
||||
|
||||
:raises keystoneauth1.exceptions.http.HttpError: An error from an
|
||||
invalid HTTP response.
|
||||
|
||||
:return: Valid EndpointData or None if not available.
|
||||
:rtype: `keystoneauth1.discover.EndpointData` or None
|
||||
"""
|
||||
min_version, max_version = discover._normalize_version_args(
|
||||
None, min_version, max_version)
|
||||
|
||||
# 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:
|
||||
endpoint_data = discover.EndpointData(
|
||||
service_url=self.auth_url,
|
||||
service_type=service_type or 'identity')
|
||||
project_id = None
|
||||
elif endpoint_override:
|
||||
# TODO(mordred) Make a code path that will look for a
|
||||
# matching entry in the catalog if the catalog
|
||||
# exists and fill in the interface, region_name, etc.
|
||||
# For now, just use any information the use has
|
||||
# provided.
|
||||
endpoint_data = discover.EndpointData(
|
||||
catalog_url=endpoint_override,
|
||||
interface=interface,
|
||||
region_name=region_name,
|
||||
service_name=service_name)
|
||||
# Setting an endpoint_override then calling get_endpoint_data means
|
||||
# you absolutely want the discovery info for the URL in question.
|
||||
# There are no code flows where this will happen for any other
|
||||
# reasons.
|
||||
allow_version_hack = False
|
||||
project_id = self.get_project_id(session)
|
||||
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
|
||||
|
||||
# It's possible for things higher in the stack, because of
|
||||
# defaults, to explicitly pass None.
|
||||
if not interface:
|
||||
interface = 'public'
|
||||
|
||||
service_catalog = self.get_access(session).service_catalog
|
||||
project_id = self.get_project_id(session)
|
||||
# NOTE(mordred): service_catalog.url_data_for raises if it can't
|
||||
# find a match, so this will always be a valid object.
|
||||
endpoint_data = service_catalog.endpoint_data_for(
|
||||
service_type=service_type,
|
||||
interface=interface,
|
||||
region_name=region_name,
|
||||
service_name=service_name)
|
||||
if not endpoint_data:
|
||||
return None
|
||||
|
||||
if skip_discovery:
|
||||
return endpoint_data
|
||||
|
||||
try:
|
||||
return endpoint_data.get_versioned_data(
|
||||
session,
|
||||
project_id=project_id,
|
||||
min_version=min_version,
|
||||
max_version=max_version,
|
||||
cache=self._discovery_cache,
|
||||
discover_versions=discover_versions,
|
||||
allow_version_hack=allow_version_hack, allow=allow)
|
||||
except (exceptions.DiscoveryFailure,
|
||||
exceptions.HttpError,
|
||||
exceptions.ConnectionError):
|
||||
# If a version was requested, we didn't find it, return
|
||||
# None.
|
||||
if max_version or min_version:
|
||||
return None
|
||||
# If one wasn't, then the endpoint_data we already have
|
||||
# should be fine
|
||||
return endpoint_data
|
||||
|
||||
def get_endpoint(self, session, service_type=None, interface=None,
|
||||
region_name=None, service_name=None, version=None,
|
||||
allow={}, allow_version_hack=True,
|
||||
skip_discovery=False,
|
||||
min_version=None, max_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.
|
||||
|
||||
version, min_version and max_version can all be given either as a
|
||||
string or a tuple.
|
||||
|
||||
Valid interface types: `public` or `publicURL`,
|
||||
`internal` or `internalURL`,
|
||||
`admin` or 'adminURL`
|
||||
|
||||
:param session: A session object that can be used for communication.
|
||||
:type session: keystoneauth1.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 interface: Type of endpoint. Can be a single value or a list
|
||||
of values. If it's a list of values, they will be
|
||||
looked for in order of preference. Can also be
|
||||
`keystoneauth1.plugin.AUTH_INTERFACE` to indicate
|
||||
that the auth_url should be used instead of the
|
||||
value in the catalog. (optional, 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 version: The minimum version number required for this
|
||||
endpoint. (optional)
|
||||
:param dict allow: Extra filters to pass when discovering API
|
||||
versions. (optional)
|
||||
:param bool allow_version_hack: Allow keystoneauth to hack up catalog
|
||||
URLS to support older schemes.
|
||||
(optional, default True)
|
||||
:param bool skip_discovery: Whether to skip version discovery even
|
||||
if a version has been given. This is useful
|
||||
if endpoint_override or similar has been
|
||||
given and grabbing additional information
|
||||
about the endpoint is not useful.
|
||||
:param min_version: The minimum version that is acceptable. Mutually
|
||||
exclusive with version. If min_version is given
|
||||
with no max_version it is as if max version is
|
||||
'latest'. (optional)
|
||||
:param max_version: The maximum version that is acceptable. Mutually
|
||||
exclusive with version. If min_version is given
|
||||
with no max_version it is as if max version is
|
||||
'latest'. (optional)
|
||||
|
||||
:raises keystoneauth1.exceptions.http.HttpError: An error from an
|
||||
invalid HTTP response.
|
||||
|
||||
:return: A valid endpoint URL or None if not available.
|
||||
:rtype: string or None
|
||||
"""
|
||||
# Explode `version` into min_version and max_version - everything below
|
||||
# here uses the latter rather than the former.
|
||||
min_version, max_version = discover._normalize_version_args(
|
||||
version, min_version, max_version)
|
||||
# Set discover_versions to False since we're only going to return
|
||||
# a URL. Fetching the microversion data would be needlessly
|
||||
# expensive in the common case. However, discover_versions=False
|
||||
# will still run discovery if the version requested is not the
|
||||
# version in the catalog.
|
||||
endpoint_data = self.get_endpoint_data(
|
||||
session, service_type=service_type, interface=interface,
|
||||
region_name=region_name, service_name=service_name,
|
||||
allow=allow, min_version=min_version, max_version=max_version,
|
||||
discover_versions=False, skip_discovery=skip_discovery,
|
||||
allow_version_hack=allow_version_hack, **kwargs)
|
||||
return endpoint_data.url if endpoint_data else None
|
||||
|
||||
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: keystoneauth1.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 keystoneauth1.exceptions.discovery.DiscoveryFailure:
|
||||
if for some reason the lookup fails.
|
||||
:raises keystoneauth1.exceptions.http.HttpError: An error from an
|
||||
invalid HTTP response.
|
||||
|
||||
:returns: A discovery object with the results of looking up that URL.
|
||||
"""
|
||||
return discover.get_discovery(session=session, url=url,
|
||||
cache=self._discovery_cache,
|
||||
authenticated=authenticated)
|
||||
|
||||
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 overridden 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(elements.items()):
|
||||
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
|
@ -1,21 +0,0 @@
|
||||
# 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',
|
||||
)
|
@ -1,215 +0,0 @@
|
||||
# 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 dependent.
|
||||
|
||||
Internally we will construct a version dependent 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,
|
||||
reauthenticate=True):
|
||||
super(BaseGenericPlugin, self).__init__(auth_url=auth_url,
|
||||
reauthenticate=reauthenticate)
|
||||
|
||||
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 parameters.
|
||||
|
||||
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: keystoneauth1.session.Session
|
||||
:param tuple version: A tuple of the API version at the URL.
|
||||
:param str url: The base URL for this version.
|
||||
:param str 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):
|
||||
"""Return the parameters that are common to v2 plugins."""
|
||||
return {'trust_id': self._trust_id,
|
||||
'tenant_id': self._project_id,
|
||||
'tenant_name': self._project_name,
|
||||
'reauthenticate': self.reauthenticate}
|
||||
|
||||
@property
|
||||
def _v3_params(self):
|
||||
"""Return the parameters that are common to v3 plugins."""
|
||||
return {'trust_id': self._trust_id,
|
||||
'project_id': self._project_id,
|
||||
'project_name': self._project_name,
|
||||
'project_domain_id': self.project_domain_id,
|
||||
'project_domain_name': self.project_domain_name,
|
||||
'domain_id': self._domain_id,
|
||||
'domain_name': self._domain_name,
|
||||
'reauthenticate': self.reauthenticate}
|
||||
|
||||
@property
|
||||
def project_domain_id(self):
|
||||
return self._project_domain_id or self._default_domain_id
|
||||
|
||||
@project_domain_id.setter
|
||||
def project_domain_id(self, value):
|
||||
self._project_domain_id = value
|
||||
|
||||
@property
|
||||
def project_domain_name(self):
|
||||
return self._project_domain_name or self._default_domain_name
|
||||
|
||||
@project_domain_name.setter
|
||||
def project_domain_name(self, value):
|
||||
self._project_domain_name = value
|
||||
|
||||
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('Failed to discover available identity versions when '
|
||||
'contacting %s. Attempting to parse version from URL.',
|
||||
self.auth_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)
|
||||
|
||||
def get_cache_id_elements(self, _implemented=False):
|
||||
# NOTE(jamielennox): implemented here is just a way to make sure that
|
||||
# something overrides this method. We don't want the base
|
||||
# implementation to respond with a dict without the subclass modifying
|
||||
# it to add their own data in case the subclass doesn't support caching
|
||||
if not _implemented:
|
||||
raise NotImplemented()
|
||||
|
||||
return {'auth_url': self.auth_url,
|
||||
'project_id': self._project_id,
|
||||
'project_name': self._project_name,
|
||||
'project_domain_id': self.project_domain_id,
|
||||
'project_domain_name': self.project_domain_name,
|
||||
'domain_id': self._domain_id,
|
||||
'domain_name': self._domain_name,
|
||||
'trust_id': self._trust_id}
|
@ -1,90 +0,0 @@
|
||||
# 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)
|
||||
|
||||
@property
|
||||
def user_domain_id(self):
|
||||
return self._user_domain_id or self._default_domain_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_name or self._default_domain_name
|
||||
|
||||
@user_domain_name.setter
|
||||
def user_domain_name(self, value):
|
||||
self._user_domain_name = value
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
elements = super(Password, self).get_cache_id_elements(
|
||||
_implemented=True)
|
||||
elements['username'] = self._username
|
||||
elements['user_id'] = self._user_id
|
||||
elements['password'] = self._password
|
||||
elements['user_domain_id'] = self.user_domain_id
|
||||
elements['user_domain_name'] = self.user_domain_name
|
||||
return elements
|
@ -1,39 +0,0 @@
|
||||
# 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 discover
|
||||
from keystoneauth1.identity.generic import base
|
||||
from keystoneauth1.identity import v2
|
||||
from keystoneauth1.identity import v3
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
elements = super(Token, self).get_cache_id_elements(_implemented=True)
|
||||
elements['token'] = self._token
|
||||
return elements
|
@ -1,178 +0,0 @@
|
||||
# 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):
|
||||
"""Return true if parameters can be used to 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}
|
@ -1,46 +0,0 @@
|
||||
# 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.oidc import * # noqa
|
||||
from keystoneauth1.identity.v3.password import * # noqa
|
||||
from keystoneauth1.identity.v3.token import * # noqa
|
||||
from keystoneauth1.identity.v3.totp import * # noqa
|
||||
from keystoneauth1.identity.v3.tokenless_auth import * # noqa
|
||||
|
||||
|
||||
__all__ = ('Auth',
|
||||
'AuthConstructor',
|
||||
'AuthMethod',
|
||||
'BaseAuth',
|
||||
|
||||
'FederationBaseAuth',
|
||||
|
||||
'Keystone2Keystone',
|
||||
|
||||
'Password',
|
||||
'PasswordMethod',
|
||||
|
||||
'Token',
|
||||
'TokenMethod',
|
||||
|
||||
'OidcAccessToken',
|
||||
'OidcAuthorizationCode',
|
||||
'OidcClientCredentials',
|
||||
'OidcPassword',
|
||||
|
||||
'TOTPMethod',
|
||||
'TOTP',
|
||||
|
||||
'TokenlessAuth')
|
@ -1,282 +0,0 @@
|
||||
# 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 json
|
||||
|
||||
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 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):
|
||||
"""Return true if parameters can be used to 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:
|
||||
_logger.debug(json.dumps(resp.json()))
|
||||
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 NotImplementedError:
|
||||
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: keystoneauth1.session.Session
|
||||
:param base.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 NotImplementedError()
|
||||
|
||||
|
||||
@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)
|
@ -1,115 +0,0 @@
|
||||
# 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: keystoneauth1.session.Session
|
||||
|
||||
:returns: a token data representation
|
||||
:rtype: :py:class:`keystoneauth1.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):
|
||||
"""Federation authentication plugin.
|
||||
|
||||
: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
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, auth_url, identity_provider, protocol, **kwargs):
|
||||
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
|
@ -1,173 +0,0 @@
|
||||
# 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 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."""
|
||||
|
||||
HTTP_MOVED_TEMPORARILY = 302
|
||||
HTTP_SEE_OTHER = 303
|
||||
|
||||
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/
|
||||
<idp>/protocols/<protocol_id>/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/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.
|
||||
if response.status_code in (self.HTTP_MOVED_TEMPORARILY,
|
||||
self.HTTP_SEE_OTHER):
|
||||
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)
|
@ -1,480 +0,0 @@
|
||||
# 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 warnings
|
||||
|
||||
from positional import positional
|
||||
import six
|
||||
|
||||
from keystoneauth1 import _utils as utils
|
||||
from keystoneauth1 import access
|
||||
from keystoneauth1 import exceptions
|
||||
from keystoneauth1.identity.v3 import federation
|
||||
|
||||
_logger = utils.get_logger(__name__)
|
||||
|
||||
__all__ = ('OidcAuthorizationCode',
|
||||
'OidcClientCredentials',
|
||||
'OidcPassword',
|
||||
'OidcAccessToken')
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
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``
|
||||
"""
|
||||
|
||||
grant_type = None
|
||||
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
client_id, client_secret,
|
||||
access_token_type,
|
||||
scope="openid profile",
|
||||
access_token_endpoint=None,
|
||||
discovery_endpoint=None,
|
||||
grant_type=None,
|
||||
**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_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
|
||||
|
||||
:param access_token_endpoint: OpenID Connect Provider Token Endpoint,
|
||||
for example:
|
||||
https://localhost:8020/oidc/OP/token
|
||||
Note that if a discovery document is
|
||||
provided this value will override
|
||||
the discovered one.
|
||||
:type access_token_endpoint: string
|
||||
|
||||
:param discovery_endpoint: OpenID Connect Discovery Document URL,
|
||||
for example:
|
||||
https://localhost:8020/oidc/.well-known/openid-configuration
|
||||
:type access_token_endpoint: string
|
||||
|
||||
:param scope: OpenID Connect scope that is requested from OP,
|
||||
for example: "openid profile email", defaults to
|
||||
"openid profile". Note that OpenID Connect specification
|
||||
states that "openid" must be always specified.
|
||||
:type scope: string
|
||||
"""
|
||||
super(_OidcBase, self).__init__(auth_url, identity_provider, protocol,
|
||||
**kwargs)
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
self.discovery_endpoint = discovery_endpoint
|
||||
self._discovery_document = {}
|
||||
self.access_token_endpoint = access_token_endpoint
|
||||
|
||||
self.access_token_type = access_token_type
|
||||
self.scope = scope
|
||||
|
||||
if grant_type is not None:
|
||||
if grant_type != self.grant_type:
|
||||
raise exceptions.OidcGrantTypeMissmatch()
|
||||
warnings.warn("Passing grant_type as an argument has been "
|
||||
"deprecated as it is now defined in the plugin "
|
||||
"itself. You should stop passing this argument "
|
||||
"to the plugin, as it will be ignored, since you "
|
||||
"cannot pass a free text string as a grant_type. "
|
||||
"This argument will be dropped from the plugin in "
|
||||
"July 2017 or with the next major release of "
|
||||
"keystoneauth (3.0.0)",
|
||||
DeprecationWarning)
|
||||
|
||||
def _get_discovery_document(self, session):
|
||||
"""Get the contents of the OpenID Connect Discovery Document.
|
||||
|
||||
This method grabs the contents of the OpenID Connect Discovery Document
|
||||
if a discovery_endpoint was passed to the constructor and returns it as
|
||||
a dict, otherwise returns an empty dict. Note that it will fetch the
|
||||
discovery document only once, so subsequent calls to this method will
|
||||
return the cached result, if any.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:returns: a python dictionary containing the discovery document if any,
|
||||
otherwise it will return an empty dict.
|
||||
:rtype: dict
|
||||
"""
|
||||
if (self.discovery_endpoint is not None and
|
||||
not self._discovery_document):
|
||||
try:
|
||||
resp = session.get(self.discovery_endpoint,
|
||||
authenticated=False)
|
||||
except exceptions.HttpError:
|
||||
_logger.error("Cannot fetch discovery document %(discovery)s" %
|
||||
{"discovery": self.discovery_endpoint})
|
||||
raise
|
||||
|
||||
try:
|
||||
self._discovery_document = resp.json()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not self._discovery_document:
|
||||
raise exceptions.InvalidOidcDiscoveryDocument()
|
||||
|
||||
return self._discovery_document
|
||||
|
||||
def _get_access_token_endpoint(self, session):
|
||||
"""Get the "token_endpoint" for the OpenID Connect flow.
|
||||
|
||||
This method will return the correct access token endpoint to be used.
|
||||
If the user has explicitly passed an access_token_endpoint to the
|
||||
constructor that will be returned. If there is no explicit endpoint and
|
||||
a discovery url is provided, it will try to get it from the discovery
|
||||
document. If nothing is found, an exception will be raised.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:return: the endpoint to use
|
||||
:rtype: string or None if no endpoint is found
|
||||
"""
|
||||
if self.access_token_endpoint is not None:
|
||||
return self.access_token_endpoint
|
||||
|
||||
discovery = self._get_discovery_document(session)
|
||||
endpoint = discovery.get("token_endpoint")
|
||||
if endpoint is None:
|
||||
raise exceptions.OidcAccessTokenEndpointNotFound()
|
||||
return endpoint
|
||||
|
||||
def _get_access_token(self, session, payload):
|
||||
"""Exchange a variety of user supplied values for an access token.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
: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
|
||||
"""
|
||||
client_auth = (self.client_id, self.client_secret)
|
||||
access_token_endpoint = self._get_access_token_endpoint(session)
|
||||
|
||||
op_response = session.post(access_token_endpoint,
|
||||
requests_auth=client_auth,
|
||||
data=payload,
|
||||
authenticated=False)
|
||||
access_token = op_response.json()[self.access_token_type]
|
||||
return access_token
|
||||
|
||||
def _get_keystone_token(self, session, access_token):
|
||||
r"""Exchange an access 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: keystoneauth1.session.Session
|
||||
|
||||
:param access_token: The OpenID Connect access token.
|
||||
:type access_token: str
|
||||
"""
|
||||
# use access token against protected URL
|
||||
headers = {'Authorization': 'Bearer ' + access_token}
|
||||
auth_response = session.post(self.federated_token_url,
|
||||
headers=headers,
|
||||
authenticated=False)
|
||||
return auth_response
|
||||
|
||||
def get_unscoped_auth_ref(self, session):
|
||||
"""Authenticate with OpenID Connect and get back claims.
|
||||
|
||||
This is a multi-step process:
|
||||
|
||||
1.- An access token must be retrieved from the server. In order to do
|
||||
so, we need to exchange an authorization grant or refresh token
|
||||
with the token endpoint in order to obtain an access token. The
|
||||
authorization grant varies from plugin to plugin.
|
||||
|
||||
2.- 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`
|
||||
"""
|
||||
# First of all, check if the grant type is supported
|
||||
discovery = self._get_discovery_document(session)
|
||||
grant_types = discovery.get("grant_types_supported")
|
||||
if (grant_types and
|
||||
self.grant_type is not None and
|
||||
self.grant_type not in grant_types):
|
||||
raise exceptions.OidcPluginNotSupported()
|
||||
|
||||
# Get the payload
|
||||
payload = self.get_payload(session)
|
||||
payload.setdefault('grant_type', self.grant_type)
|
||||
|
||||
# get an access token
|
||||
access_token = self._get_access_token(session, payload)
|
||||
|
||||
response = self._get_keystone_token(session, access_token)
|
||||
|
||||
# grab the unscoped token
|
||||
return access.create(resp=response)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_payload(self, session):
|
||||
"""Get the plugin specific payload for obtainin an access token.
|
||||
|
||||
OpenID Connect supports different grant types. This method should
|
||||
prepare the payload that needs to be exchanged with the server in
|
||||
order to get an access token for the particular grant type that the
|
||||
plugin is implementing.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:returns: a python dictionary containing the payload to be exchanged
|
||||
:rtype: dict
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class OidcPassword(_OidcBase):
|
||||
"""Implementation for OpenID Connect Resource Owner Password Credential."""
|
||||
|
||||
grant_type = "password"
|
||||
|
||||
@positional(4)
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
client_id, client_secret,
|
||||
access_token_endpoint=None,
|
||||
discovery_endpoint=None,
|
||||
access_token_type='access_token',
|
||||
username=None, password=None,
|
||||
**kwargs):
|
||||
"""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
|
||||
"""
|
||||
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,
|
||||
discovery_endpoint=discovery_endpoint,
|
||||
access_token_type=access_token_type,
|
||||
**kwargs)
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def get_payload(self, session):
|
||||
"""Get an authorization grant for the "password" grant type.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:returns: a python dictionary containing the payload to be exchanged
|
||||
:rtype: dict
|
||||
"""
|
||||
payload = {'username': self.username,
|
||||
'password': self.password,
|
||||
'scope': self.scope}
|
||||
return payload
|
||||
|
||||
|
||||
class OidcClientCredentials(_OidcBase):
|
||||
"""Implementation for OpenID Connect Client Credentials."""
|
||||
|
||||
grant_type = 'client_credentials'
|
||||
|
||||
@positional(4)
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
client_id, client_secret,
|
||||
access_token_endpoint=None,
|
||||
discovery_endpoint=None,
|
||||
access_token_type='access_token',
|
||||
**kwargs):
|
||||
"""The OpenID Client Credentials expects the following.
|
||||
|
||||
:param client_id: Client ID used to authenticate
|
||||
:type username: string
|
||||
|
||||
:param client_secret: Client Secret used to authenticate
|
||||
:type password: string
|
||||
"""
|
||||
super(OidcClientCredentials, 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,
|
||||
discovery_endpoint=discovery_endpoint,
|
||||
access_token_type=access_token_type,
|
||||
**kwargs)
|
||||
|
||||
def get_payload(self, session):
|
||||
"""Get an authorization grant for the client credentials grant type.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:returns: a python dictionary containing the payload to be exchanged
|
||||
:rtype: dict
|
||||
"""
|
||||
payload = {'scope': self.scope}
|
||||
return payload
|
||||
|
||||
|
||||
class OidcAuthorizationCode(_OidcBase):
|
||||
"""Implementation for OpenID Connect Authorization Code."""
|
||||
|
||||
grant_type = 'authorization_code'
|
||||
|
||||
@positional(4)
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
client_id, client_secret,
|
||||
access_token_endpoint=None,
|
||||
discovery_endpoint=None,
|
||||
access_token_type='access_token',
|
||||
redirect_uri=None, code=None, **kwargs):
|
||||
"""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,
|
||||
discovery_endpoint=discovery_endpoint,
|
||||
access_token_type=access_token_type,
|
||||
**kwargs)
|
||||
self.redirect_uri = redirect_uri
|
||||
self.code = code
|
||||
|
||||
def get_payload(self, session):
|
||||
"""Get an authorization grant for the "authorization_code" grant type.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:returns: a python dictionary containing the payload to be exchanged
|
||||
:rtype: dict
|
||||
"""
|
||||
payload = {'redirect_uri': self.redirect_uri, 'code': self.code}
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class OidcAccessToken(_OidcBase):
|
||||
"""Implementation for OpenID Connect access token reuse."""
|
||||
|
||||
@positional(5)
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
access_token, **kwargs):
|
||||
"""The OpenID Connect plugin based on the Access Token.
|
||||
|
||||
It 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 access_token: OpenID Connect Access token
|
||||
:type access_token: string
|
||||
"""
|
||||
super(OidcAccessToken, self).__init__(auth_url, identity_provider,
|
||||
protocol,
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
access_token_endpoint=None,
|
||||
access_token_type=None,
|
||||
**kwargs)
|
||||
self.access_token = access_token
|
||||
|
||||
def get_payload(self, session):
|
||||
"""OidcAccessToken does not require a payload."""
|
||||
return {}
|
||||
|
||||
def get_unscoped_auth_ref(self, session):
|
||||
"""Authenticate with OpenID Connect and get back claims.
|
||||
|
||||
We 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: keystoneclient.session.Session
|
||||
|
||||
:returns: a token data representation
|
||||
:rtype: :py:class:`keystoneauth1.access.AccessInfoV3`
|
||||
"""
|
||||
response = self._get_keystone_token(session, self.access_token)
|
||||
return access.create(resp=response)
|
@ -1,75 +0,0 @@
|
||||
# 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
|
@ -1,54 +0,0 @@
|
||||
# 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)
|
@ -1,115 +0,0 @@
|
||||
# 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 import _utils as utils
|
||||
from keystoneauth1 import plugin
|
||||
|
||||
LOG = utils.get_logger(__name__)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class TokenlessAuth(plugin.BaseAuthPlugin):
|
||||
"""A plugin for authenticating with Tokenless Auth.
|
||||
|
||||
This is for Tokenless Authentication. Scoped information
|
||||
like domain name and project ID will be passed in the headers and
|
||||
token validation request will be authenticated based on
|
||||
the provided HTTPS certificate along with the scope information.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_url,
|
||||
domain_id=None,
|
||||
domain_name=None,
|
||||
project_id=None,
|
||||
project_name=None,
|
||||
project_domain_id=None,
|
||||
project_domain_name=None):
|
||||
"""A init method for TokenlessAuth.
|
||||
|
||||
:param string auth_url: Identity service endpoint for authentication.
|
||||
The URL must include a version or any request
|
||||
will result in a 404 NotFound error.
|
||||
: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.
|
||||
"""
|
||||
self.auth_url = auth_url
|
||||
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
|
||||
|
||||
def get_headers(self, session, **kwargs):
|
||||
"""Fetch authentication headers for message.
|
||||
|
||||
This is to override the default get_headers method to provide
|
||||
tokenless auth scope headers if token is not provided in the
|
||||
session.
|
||||
|
||||
: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
|
||||
"""
|
||||
scope_headers = {}
|
||||
if self.project_id:
|
||||
scope_headers['X-Project-Id'] = self.project_id
|
||||
elif self.project_name:
|
||||
scope_headers['X-Project-Name'] = self.project_name
|
||||
if self.project_domain_id:
|
||||
scope_headers['X-Project-Domain-Id'] = (
|
||||
self.project_domain_id)
|
||||
elif self.project_domain_name:
|
||||
scope_headers['X-Project-Domain-Name'] = (
|
||||
self.project_domain_name)
|
||||
else:
|
||||
LOG.warning(
|
||||
'Neither Project Domain ID nor Project Domain Name was '
|
||||
'provided.')
|
||||
return None
|
||||
elif self.domain_id:
|
||||
scope_headers['X-Domain-Id'] = self.domain_id
|
||||
elif self.domain_name:
|
||||
scope_headers['X-Domain-Name'] = self.domain_name
|
||||
else:
|
||||
LOG.warning(
|
||||
'Neither Project nor Domain scope was provided.')
|
||||
return None
|
||||
return scope_headers
|
||||
|
||||
def get_endpoint(self, session, service_type=None, **kwargs):
|
||||
"""Return a valid endpoint for a service.
|
||||
|
||||
:param session: A session object that can be used for communication.
|
||||
:type session: keystoneauth1.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.
|
||||
:return: A valid endpoint URL or None if not available.
|
||||
:rtype: string or None
|
||||
"""
|
||||
if (service_type is plugin.AUTH_INTERFACE
|
||||
or service_type.lower() == 'identity'):
|
||||
return self.auth_url
|
||||
|
||||
return None
|
@ -1,81 +0,0 @@
|
||||
# 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
|
||||
|
||||
from keystoneauth1.identity.v3 import base
|
||||
|
||||
|
||||
__all__ = ('TOTPMethod', 'TOTP')
|
||||
|
||||
|
||||
class TOTPMethod(base.AuthMethod):
|
||||
"""Construct a User/Passcode based authentication method.
|
||||
|
||||
:param string passcode: TOTP passcode 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',
|
||||
'passcode']
|
||||
|
||||
def get_auth_data(self, session, auth, headers, **kwargs):
|
||||
user = {'passcode': self.passcode}
|
||||
|
||||
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 'totp', {'user': user}
|
||||
|
||||
def get_cache_id_elements(self):
|
||||
# NOTE(gyee): passcode is not static so we cannot use it as part of
|
||||
# the key in caching.
|
||||
params = copy.copy(self._method_parameters)
|
||||
params.remove('passcode')
|
||||
return dict(('totp_%s' % p, getattr(self, p))
|
||||
for p in self._method_parameters)
|
||||
|
||||
|
||||
class TOTP(base.AuthConstructor):
|
||||
"""A plugin for authenticating with a username and TOTP passcode.
|
||||
|
||||
:param string auth_url: Identity service endpoint for authentication.
|
||||
:param string passcode: TOTP passcode 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 = TOTPMethod
|
@ -1,85 +0,0 @@
|
||||
# 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 adapter
|
||||
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
|
||||
|
||||
register_adapter_argparse_arguments = adapter.register_argparse_arguments
|
||||
register_service_adapter_argparse_arguments = (
|
||||
adapter.register_service_argparse_arguments)
|
||||
register_adapter_conf_options = adapter.register_conf_options
|
||||
load_adapter_from_conf_options = adapter.load_from_conf_options
|
||||
get_adapter_conf_options = adapter.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',
|
||||
|
||||
# adapter
|
||||
'register_adapter_argparse_arguments',
|
||||
'register_service_adapter_argparse_arguments',
|
||||
'register_adapter_conf_options',
|
||||
'load_adapter_from_conf_options',
|
||||
'get_adapter_conf_options',
|
||||
|
||||
# loading.opts
|
||||
'Opt',
|
||||
)
|
@ -1,46 +0,0 @@
|
||||
# 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):
|
||||
"""Use an existing token and a known endpoint to perform requests.
|
||||
|
||||
This plugin is primarily useful for development or for use with identity
|
||||
service ADMIN tokens. Because this token is used directly there is no
|
||||
fetching a service catalog or determining scope information and so it
|
||||
cannot be used by clients that expect use this scope information.
|
||||
|
||||
Because there is no service catalog the endpoint that is supplied with
|
||||
initialization is used for all operations performed with this plugin so
|
||||
must be the full base URL to an actual service.
|
||||
"""
|
||||
|
||||
@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
|
@ -1,75 +0,0 @@
|
||||
# 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):
|
||||
"""Given an existing token rescope it to another target.
|
||||
|
||||
This plugin uses the Identity service's rescope mechanism to get a new
|
||||
token based upon an existing token. Because an auth plugin requires a
|
||||
service catalog and scope information it is often easier to fetch a new
|
||||
token based on an existing one than validate and reuse the one you already
|
||||
have.
|
||||
|
||||
As a generic plugin this plugin is identity version independent and will
|
||||
discover available versions before use. This means it expects to be
|
||||
providen an unversioned URL to operate against.
|
||||
"""
|
||||
|
||||
@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):
|
||||
"""Authenticate via a username and password.
|
||||
|
||||
Authenticate to the identity service using an inbuilt username and
|
||||
password. This is the standard and most common form of authentication.
|
||||
|
||||
As a generic plugin this plugin is identity version independent and will
|
||||
discover available versions before use. This means it expects to be
|
||||
providen an unversioned URL to operate against.
|
||||
"""
|
||||
|
||||
@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,
|
||||
prompt='Password: ',
|
||||
help="User's password"),
|
||||
])
|
||||
return options
|
@ -1,53 +0,0 @@
|
||||
# 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,
|
||||
prompt='Password: ',
|
||||
help='Password to use'),
|
||||
])
|
||||
|
||||
return options
|
@ -1,256 +0,0 @@
|
||||
# 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
|
||||
|
||||
|
||||
def _add_common_identity_options(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"),
|
||||
])
|
||||
|
||||
|
||||
def _assert_identity_options(options):
|
||||
if (options.get('username') and
|
||||
not (options.get('user_domain_name') or
|
||||
options.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)
|
||||
|
||||
|
||||
class Password(loading.BaseV3Loader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return identity.V3Password
|
||||
|
||||
def get_options(self):
|
||||
options = super(Password, self).get_options()
|
||||
_add_common_identity_options(options)
|
||||
|
||||
options.extend([
|
||||
loading.Opt('password',
|
||||
secret=True,
|
||||
prompt='Password: ',
|
||||
help="User's password"),
|
||||
])
|
||||
|
||||
return options
|
||||
|
||||
def load_from_options(self, **kwargs):
|
||||
_assert_identity_options(kwargs)
|
||||
|
||||
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 load_from_options(self, **kwargs):
|
||||
if not (kwargs.get('access_token_endpoint') or
|
||||
kwargs.get('discovery_endpoint')):
|
||||
m = ("You have to specify either an 'access-token-endpoint' or "
|
||||
"a 'discovery-endpoint'.")
|
||||
raise exceptions.OptionError(m)
|
||||
|
||||
return super(_OpenIDConnectBase, self).load_from_options(**kwargs)
|
||||
|
||||
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('openid-scope', default="openid profile",
|
||||
dest="scope",
|
||||
help='OpenID Connect scope that is requested from '
|
||||
'authorization server. Note that the OpenID '
|
||||
'Connect specification states that "openid" '
|
||||
'must be always specified.'),
|
||||
loading.Opt('access-token-endpoint',
|
||||
help='OpenID Connect Provider Token Endpoint. Note '
|
||||
'that if a discovery document is being passed this '
|
||||
'option will override the endpoint provided by the '
|
||||
'server in the discovery document.'),
|
||||
loading.Opt('discovery-endpoint',
|
||||
help='OpenID Connect Discovery Document URL. '
|
||||
'The discovery document will be used to obtain the '
|
||||
'values of the access token endpoint and the '
|
||||
'authentication endpoint. This URL should look like '
|
||||
'https://idp.example.org/.well-known/'
|
||||
'openid-configuration'),
|
||||
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 OpenIDConnectClientCredentials(_OpenIDConnectBase):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return identity.V3OidcClientCredentials
|
||||
|
||||
def get_options(self):
|
||||
options = super(OpenIDConnectClientCredentials, self).get_options()
|
||||
|
||||
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', required=True),
|
||||
loading.Opt('password', secret=True,
|
||||
help='Password', required=True),
|
||||
])
|
||||
|
||||
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('code', secret=True, required=True,
|
||||
deprecated=[loading.Opt('authorization-code')],
|
||||
help='OAuth 2.0 Authorization Code'),
|
||||
])
|
||||
|
||||
return options
|
||||
|
||||
|
||||
class OpenIDConnectAccessToken(loading.BaseFederationLoader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return identity.V3OidcAccessToken
|
||||
|
||||
def get_options(self):
|
||||
options = super(OpenIDConnectAccessToken, self).get_options()
|
||||
|
||||
options.extend([
|
||||
loading.Opt('access-token', secret=True, required=True,
|
||||
help='OAuth 2.0 Access Token'),
|
||||
])
|
||||
return options
|
||||
|
||||
|
||||
class TOTP(loading.BaseV3Loader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return identity.V3TOTP
|
||||
|
||||
def get_options(self):
|
||||
options = super(TOTP, self).get_options()
|
||||
_add_common_identity_options(options)
|
||||
|
||||
options.extend([
|
||||
loading.Opt('passcode', secret=True, help="User's TOTP passcode"),
|
||||
])
|
||||
|
||||
return options
|
||||
|
||||
def load_from_options(self, **kwargs):
|
||||
_assert_identity_options(kwargs)
|
||||
|
||||
return super(TOTP, self).load_from_options(**kwargs)
|
||||
|
||||
|
||||
class TokenlessAuth(loading.BaseLoader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return identity.V3TokenlessAuth
|
||||
|
||||
def get_options(self):
|
||||
options = super(TokenlessAuth, self).get_options()
|
||||
|
||||
options.extend([
|
||||
loading.Opt('auth-url', required=True,
|
||||
help='Authentication URL'),
|
||||
loading.Opt('domain-id', help='Domain ID to scope to'),
|
||||
loading.Opt('domain-name', help='Domain name to scope to'),
|
||||
loading.Opt('project-id', help='Project ID to scope to'),
|
||||
loading.Opt('project-name', help='Project name to scope to'),
|
||||
loading.Opt('project-domain-id',
|
||||
help='Domain ID containing project'),
|
||||
loading.Opt('project-domain-name',
|
||||
help='Domain name containing project'),
|
||||
])
|
||||
|
||||
return options
|
||||
|
||||
def load_from_options(self, **kwargs):
|
||||
if (not kwargs.get('domain_id') and
|
||||
not kwargs.get('domain_name') and
|
||||
not kwargs.get('project_id') and
|
||||
not kwargs.get('project_name') or
|
||||
(kwargs.get('project_name') and
|
||||
not (kwargs.get('project_domain_name') or
|
||||
kwargs.get('project_domain_id')))):
|
||||
m = ('You need to provide either a domain_name, domain_id, '
|
||||
'project_id or project_name. '
|
||||
'If 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(TokenlessAuth, self).load_from_options(**kwargs)
|
@ -1,33 +0,0 @@
|
||||
# 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 noauth
|
||||
|
||||
|
||||
class NoAuth(loading.BaseLoader):
|
||||
"""Use no tokens to perform requests.
|
||||
|
||||
This must be used together with adapter.Adapter.endpoint_override
|
||||
to instantiate clients for services deployed in noauth/standalone mode.
|
||||
|
||||
There is no fetching a service catalog or determining scope information
|
||||
and so it cannot be used by clients that expect use this scope information.
|
||||
|
||||
"""
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return noauth.NoAuth
|
||||
|
||||
def get_options(self):
|
||||
return []
|
@ -1,40 +0,0 @@
|
||||
# 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.
|
||||
|
||||
cfg = None
|
||||
_NOT_FOUND = object()
|
||||
|
||||
|
||||
def get_oslo_config():
|
||||
"""Runtime load the oslo.config object.
|
||||
|
||||
In performance optimization of openstackclient it was determined that even
|
||||
optimistically loading oslo.config if available had a performance cost.
|
||||
Given that we used to only raise the ImportError when the function was
|
||||
called also attempt to do the import to do everything at runtime.
|
||||
"""
|
||||
global cfg
|
||||
|
||||
# First Call
|
||||
if not cfg:
|
||||
try:
|
||||
from oslo_config import cfg
|
||||
except ImportError:
|
||||
cfg = _NOT_FOUND
|
||||
|
||||
if cfg is _NOT_FOUND:
|
||||
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. ")
|
||||
|
||||
return cfg
|
@ -1,209 +0,0 @@
|
||||
# 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 adapter
|
||||
from keystoneauth1.loading import _utils
|
||||
from keystoneauth1.loading import base
|
||||
|
||||
|
||||
__all__ = ('register_argparse_arguments',
|
||||
'register_service_argparse_arguments',
|
||||
'register_conf_options',
|
||||
'load_from_conf_options',
|
||||
'get_conf_options')
|
||||
|
||||
|
||||
class Adapter(base.BaseLoader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return adapter.Adapter
|
||||
|
||||
def get_options(self):
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_conf_options():
|
||||
"""Get oslo_config options that are needed for a :py:class:`.Adapter`.
|
||||
|
||||
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:
|
||||
:service_type: The default service_type for URL discovery.
|
||||
:service_name: The default service_name for URL discovery.
|
||||
:interface: The default interface for URL discovery.
|
||||
(deprecated)
|
||||
:valid_interfaces: List of acceptable interfaces for URL
|
||||
discovery. Can be a list of any of
|
||||
'public', 'internal' or 'admin'.
|
||||
:region_name: The default region_name for URL discovery.
|
||||
:endpoint_override: Always use this endpoint URL for requests
|
||||
for this client.
|
||||
:version: The minimum version restricted to a given Major
|
||||
API. Mutually exclusive with min_version and
|
||||
max_version.
|
||||
:min_version: The minimum major version of a given API,
|
||||
intended to be used as the lower bound of a
|
||||
range with max_version. Mutually exclusive with
|
||||
version. If min_version is given with no
|
||||
max_version it is as if max version is
|
||||
'latest'.
|
||||
:max_version: The maximum major version of a given API,
|
||||
intended to be used as the upper bound of a
|
||||
range with min_version. Mutually exclusive with
|
||||
version.
|
||||
|
||||
:returns: A list of oslo_config options.
|
||||
"""
|
||||
cfg = _utils.get_oslo_config()
|
||||
|
||||
return [cfg.StrOpt('service-type',
|
||||
help='The default service_type for endpoint URL '
|
||||
'discovery.'),
|
||||
cfg.StrOpt('service-name',
|
||||
help='The default service_name for endpoint URL '
|
||||
'discovery.'),
|
||||
cfg.StrOpt('interface',
|
||||
help='The default interface for endpoint URL '
|
||||
'discovery.',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_reason='Using valid-interfaces is'
|
||||
' preferrable because it is'
|
||||
' capable of accepting a list of'
|
||||
' possible interfaces.'),
|
||||
cfg.ListOpt('valid-interfaces',
|
||||
help='List of interfaces, in order of preference, '
|
||||
'for endpoint URL.'),
|
||||
cfg.StrOpt('region-name',
|
||||
help='The default region_name for endpoint URL '
|
||||
'discovery.'),
|
||||
cfg.StrOpt('endpoint-override',
|
||||
help='Always use this endpoint URL for requests '
|
||||
'for this client.'),
|
||||
cfg.StrOpt('version',
|
||||
help='Minimum Major API version within a given '
|
||||
'Major API version for endpoint URL '
|
||||
'discovery. Mutually exclusive with '
|
||||
'min_version and max_version'),
|
||||
cfg.StrOpt('min-version',
|
||||
help='The minimum major version of a given API, '
|
||||
'intended to be used as the lower bound of a '
|
||||
'range with max_version. Mutually exclusive '
|
||||
'with version. If min_version is given with '
|
||||
'no max_version it is as if max version is '
|
||||
'"latest".'),
|
||||
cfg.StrOpt('max-version',
|
||||
help='The maximum major version of a given API, '
|
||||
'intended to be used as the upper bound of a '
|
||||
'range with min_version. Mutually exclusive '
|
||||
'with version.'),
|
||||
]
|
||||
|
||||
def register_conf_options(self, conf, group):
|
||||
"""Register the oslo_config options that are needed for an Adapter.
|
||||
|
||||
The options that are set are:
|
||||
:service_type: The default service_type for URL discovery.
|
||||
:service_name: The default service_name for URL discovery.
|
||||
:interface: The default interface for URL discovery.
|
||||
(deprecated)
|
||||
:valid_interfaces: List of acceptable interfaces for URL
|
||||
discovery. Can be a list of any of
|
||||
'public', 'internal' or 'admin'.
|
||||
:region_name: The default region_name for URL discovery.
|
||||
:endpoint_override: Always use this endpoint URL for requests
|
||||
for this client.
|
||||
:version: The minimum version restricted to a given Major
|
||||
API. Mutually exclusive with min_version and
|
||||
max_version.
|
||||
:min_version: The minimum major version of a given API,
|
||||
intended to be used as the lower bound of a
|
||||
range with max_version. Mutually exclusive with
|
||||
version. If min_version is given with no
|
||||
max_version it is as if max version is
|
||||
'latest'.
|
||||
:max_version: The maximum major version of a given API,
|
||||
intended to be used as the upper bound of a
|
||||
range with min_version. Mutually exclusive with
|
||||
version.
|
||||
|
||||
:param oslo_config.Cfg conf: config object to register with.
|
||||
:param string group: The ini group to register options in.
|
||||
:returns: The list of options that was registered.
|
||||
"""
|
||||
opts = self.get_conf_options()
|
||||
conf.register_group(_utils.get_oslo_config().OptGroup(group))
|
||||
conf.register_opts(opts, group=group)
|
||||
return opts
|
||||
|
||||
def load_from_conf_options(self, conf, group, **kwargs):
|
||||
"""Create an Adapter 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 Adapter
|
||||
construction.
|
||||
:returns: A new Adapter object.
|
||||
:rtype: :py:class:`.Adapter`
|
||||
"""
|
||||
c = conf[group]
|
||||
|
||||
if c.valid_interfaces and c.interface:
|
||||
raise TypeError("interface and valid_interfaces are mutually"
|
||||
" exclusive. Please use valid_interfaces.")
|
||||
if c.valid_interfaces:
|
||||
for iface in c.valid_interfaces:
|
||||
if iface not in ('public', 'internal', 'admin'):
|
||||
raise TypeError("'{iface}' is not a valid value for"
|
||||
" valid_interfaces. Valid valies are"
|
||||
" public, internal or admin")
|
||||
kwargs.setdefault('interface', c.valid_interfaces)
|
||||
else:
|
||||
kwargs.setdefault('interface', c.interface)
|
||||
kwargs.setdefault('service_type', c.service_type)
|
||||
kwargs.setdefault('service_name', c.service_name)
|
||||
kwargs.setdefault('region_name', c.region_name)
|
||||
kwargs.setdefault('endpoint_override', c.endpoint_override)
|
||||
kwargs.setdefault('version', c.version)
|
||||
kwargs.setdefault('min_version', c.min_version)
|
||||
kwargs.setdefault('max_version', c.max_version)
|
||||
if kwargs['version'] and (
|
||||
kwargs['max_version'] or kwargs['min_version']):
|
||||
raise TypeError(
|
||||
"version is mutually exclusive with min_version and"
|
||||
" max_version")
|
||||
|
||||
return self.load_from_options(**kwargs)
|
||||
|
||||
|
||||
def register_argparse_arguments(*args, **kwargs):
|
||||
return adapter.register_adapter_argparse_arguments(*args, **kwargs)
|
||||
|
||||
|
||||
def register_service_argparse_arguments(*args, **kwargs):
|
||||
return adapter.register_service_adapter_argparse_arguments(*args, **kwargs)
|
||||
|
||||
|
||||
def register_conf_options(*args, **kwargs):
|
||||
return Adapter().register_conf_options(*args, **kwargs)
|
||||
|
||||
|
||||
def load_from_conf_options(*args, **kwargs):
|
||||
return Adapter().load_from_conf_options(*args, **kwargs)
|
||||
|
||||
|
||||
def get_conf_options():
|
||||
return Adapter.get_conf_options()
|
@ -1,187 +0,0 @@
|
||||
# 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 _auth_plugin_available(ext):
|
||||
"""Read the value of available for whether to load this plugin."""
|
||||
return ext.obj.available
|
||||
|
||||
|
||||
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.EnabledExtensionManager(namespace=PLUGIN_NAMESPACE,
|
||||
check_func=_auth_plugin_available,
|
||||
invoke_on_load=True,
|
||||
propagate_map_exceptions=True)
|
||||
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.EnabledExtensionManager(namespace=PLUGIN_NAMESPACE,
|
||||
check_func=_auth_plugin_available,
|
||||
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 keystoneauth1.exceptions.auth_plugins.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 keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin:
|
||||
if a plugin cannot be created.
|
||||
"""
|
||||
return get_plugin_loader(name).get_options()
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseLoader(object):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_plugin(self, **kwargs):
|
||||
"""Create a plugin from the options available for the loader.
|
||||
|
||||
Given the options that were specified by the loader create an
|
||||
appropriate plugin. You can override this function in your loader.
|
||||
|
||||
This used to be specified by providing the plugin_class property and
|
||||
this is still supported, however specifying a property didn't let you
|
||||
choose a plugin type based upon the options that were presented.
|
||||
|
||||
Override this function if you wish to return different plugins based on
|
||||
the options presented, otherwise you can simply provide the
|
||||
plugin_class property.
|
||||
|
||||
Added 2.9
|
||||
"""
|
||||
return self.plugin_class(**kwargs)
|
||||
|
||||
@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 []
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if the plugin is available for loading.
|
||||
|
||||
If a plugin is missing dependencies or for some other reason should not
|
||||
be available to the current system it should override this property and
|
||||
return False to exclude itself from the plugin list.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return True
|
||||
|
||||
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.create_plugin(**kwargs)
|
||||
|
||||
def load_from_options_getter(self, getter, **kwargs):
|
||||
"""Load a plugin from 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)
|
@ -1,105 +0,0 @@
|
||||
# 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 parser: the parser to attach argparse options to.
|
||||
:type parser: argparse.ArgumentParser
|
||||
: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: :class:`keystoneauth1.plugin.BaseAuthPlugin`
|
||||
|
||||
:raises keystoneauth1.exceptions.auth_plugins.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='<name>',
|
||||
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: :class:`keystoneauth1.plugin.BaseAuthPlugin`
|
||||
|
||||
:raises keystoneauth1.exceptions.auth_plugins.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, **kwargs)
|
@ -1,135 +0,0 @@
|
||||
# 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 plugin 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 str group: The group name that options should be read from.
|
||||
|
||||
:returns: An authentication Plugin or None if a name is not provided
|
||||
:rtype: :class:`keystoneauth1.plugin.BaseAuthPlugin`
|
||||
|
||||
:raises keystoneauth1.exceptions.auth_plugins.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)
|
@ -1,162 +0,0 @@
|
||||
# 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
|
@ -1,151 +0,0 @@
|
||||
# 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
|
||||
|
||||
from keystoneauth1.loading import _utils
|
||||
|
||||
|
||||
__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 value of name.
|
||||
:param keystoneauth1.loading.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 <metavar> 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.
|
||||
:param str prompt: If the option can be requested via a prompt (where
|
||||
appropriate) set the string that should be used to prompt with.
|
||||
"""
|
||||
|
||||
@positional()
|
||||
def __init__(self,
|
||||
name,
|
||||
type=str,
|
||||
help=None,
|
||||
secret=False,
|
||||
dest=None,
|
||||
deprecated=None,
|
||||
default=None,
|
||||
metavar=None,
|
||||
required=False,
|
||||
prompt=None):
|
||||
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
|
||||
self.prompt = prompt
|
||||
# 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 string representation of option name."""
|
||||
return '<Opt: %s>' % self.name
|
||||
|
||||
def _to_oslo_opt(self):
|
||||
cfg = _utils.get_oslo_config()
|
||||
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):
|
||||
"""Define equality operator on option parameters."""
|
||||
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)
|
||||
|
||||
# NOTE: This function is only needed by Python 2. If we get to point where
|
||||
# we don't support Python 2 anymore, this function should be removed.
|
||||
def __ne__(self, other):
|
||||
"""Define inequality operator on option parameters."""
|
||||
return not self.__eq__(other)
|
||||
|
||||
@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
|
@ -1,254 +0,0 @@
|
||||
# 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 _utils
|
||||
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='<ca-certificate>',
|
||||
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='<certificate>',
|
||||
default=os.environ.get('OS_CERT'),
|
||||
help='Defaults to env[OS_CERT].')
|
||||
|
||||
session_group.add_argument(
|
||||
'--os-key',
|
||||
metavar='<key>',
|
||||
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='<seconds>',
|
||||
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.
|
||||
"""
|
||||
cfg = _utils.get_oslo_config()
|
||||
|
||||
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(_utils.get_oslo_config().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)
|
@ -1,24 +0,0 @@
|
||||
# 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 NoAuth(plugin.BaseAuthPlugin):
|
||||
"""A provider that will always use no auth.
|
||||
|
||||
This is useful to unify session/adapter loading for services
|
||||
that might be deployed in standalone/noauth mode.
|
||||
"""
|
||||
|
||||
def get_token(self, session):
|
||||
return 'notused'
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user