diff --git a/.coveragerc b/.coveragerc index baf50977c..2f17ab1b3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,7 @@ [run] branch = True -omit = /usr*,setup.py,*egg*,.venv/*,.tox/*,keystoneclient/tests/* +source = keystoneclient +omit = keystoneclient/tests/* [report] -ignore-errors = True +ignore_errors = True diff --git a/.gitignore b/.gitignore index a74a7b333..f24746a37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ .coverage +.stestr/ +subunit.log .venv *,cover cover *.pyc .idea -*.swp +*.sw? +*.egg *~ .tox AUTHORS @@ -14,3 +17,10 @@ dist python_keystoneclient.egg-info keystoneclient/versioninfo 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 diff --git a/.gitreview b/.gitreview index 56224f5de..e26745728 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] -host=review.openstack.org +host=review.opendev.org port=29418 project=openstack/python-keystoneclient.git diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..c804401b4 --- /dev/null +++ b/.mailmap @@ -0,0 +1,6 @@ +# Format is: +# +# + + + diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 000000000..73c0a5172 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_path=${OS_TEST_PATH:-./keystoneclient/tests/unit} +top_dir=./ + diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 000000000..8b2b1a6d1 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,31 @@ +- job: + name: keystoneclient-devstack-functional + parent: devstack-minimal + timeout: 4200 + required-projects: + - openstack/keystone + - openstack/python-keystoneclient + run: playbooks/run-ds-tox.yaml + post-run: playbooks/tox-post.yaml + vars: + devstack_localrc: + USE_PYTHON3: true + devstack_services: + key: true + tox_envlist: functional + zuul_work_dir: src/opendev.org/openstack/python-keystoneclient + +- project: + templates: + - openstack-cover-jobs + - openstack-python3-jobs + - publish-openstack-docs-pti + - check-requirements + - lib-forward-testing-python3 + - release-notes-jobs-python3 + check: + jobs: + - keystoneclient-devstack-functional + gate: + jobs: + - keystoneclient-devstack-functional diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..604d3ac77 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,18 @@ +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/python-keystoneclient diff --git a/HACKING b/HACKING deleted file mode 100644 index 8f990864c..000000000 --- a/HACKING +++ /dev/null @@ -1,77 +0,0 @@ -Keystone Style Commandments -=========================== - -Step 1: Read http://www.python.org/dev/peps/pep-0008/ -Step 2: Read http://www.python.org/dev/peps/pep-0008/ again -Step 3: Read on - -Imports -------- -- thou shalt not import objects, only modules -- thou shalt not import more than one module per line -- thou shalt not make relative imports -- thou shalt organize your imports according to the following template - -:: - # vim: tabstop=4 shiftwidth=4 softtabstop=4 - {{stdlib imports in human alphabetical order}} - \n - {{third-party library imports in human alphabetical order}} - \n - {{keystoneclient imports in human alphabetical order}} - \n - \n - {{begin your code}} - - -General -------- -- thou shalt put two newlines twixt toplevel code (funcs, classes, etc) -- thou shalt put one newline twixt methods in classes and anywhere else -- thou shalt not write "except:", use "except Exception:" at the very least -- thou shalt include your name with TODOs as in "TODO(termie)" -- thou shalt not name anything the same name as a builtin or reserved word -- thou shalt not violate causality in our time cone, or else - - -Human Alphabetical Order Examples ---------------------------------- -:: - import httplib - import logging - import random - import StringIO - import testtools - import time - - import httplib2 - - from keystoneclient import exceptions - from keystoneclient import service_catalog - from keystoneclient.v2_0 import client - -Docstrings ----------- - """A one line docstring looks like this and ends in a period.""" - - - """A multiline docstring has a one-line summary, less than 80 characters. - - Then a new paragraph after a newline that explains in more detail any - general information about the function, class or method. Example usages - are also great to have here if it is a complex class for function. After - you have finished your descriptions add an extra newline and close the - quotations. - - When writing the docstring for a class, an extra line should be placed - after the closing quotations. For more in-depth explanations for these - decisions see http://www.python.org/dev/peps/pep-0257/ - - If you are going to describe parameters and return values, use Sphinx, the - appropriate syntax is as follows. - - :param foo: the foo parameter - :param bar: the bar parameter - :returns: description of the return value - - """ diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 000000000..6ea94ff6f --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,24 @@ +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 +======= + +python-keystoneclient 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/ diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e14d730d1..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -include README.rst -include AUTHORS HACKING LICENSE -include ChangeLog -include run_tests.sh tox.ini -include python-keystoneclient -include keystoneclient/versioninfo -recursive-include doc * -recursive-include tests * -recursive-include tools * diff --git a/README.rst b/README.rst index e6d29287e..6b27711a8 100644 --- a/README.rst +++ b/README.rst @@ -1,22 +1,44 @@ -Python bindings to the OpenStack Identity API (Keystone) -======================================================== +======================== +Team and repository tags +======================== -This is a client for the OpenStack Identity API, implemented by Keystone. -There's a Python API (the ``keystoneclient`` module), and a command-line script -(``keystone``). +.. image:: https://governance.openstack.org/tc/badges/python-keystoneclient.svg + :target: https://governance.openstack.org/tc/reference/tags/index.html -Development takes place via the usual OpenStack processes as outlined in the -`OpenStack wiki`_. The master repository is on GitHub__. +.. Change things from this point on -__ http://wiki.openstack.org/HowToContribute -__ http://github.com/openstack/python-keystoneclient - -This code a fork of `Rackspace's python-novaclient`__ which is in turn a fork -of `Jacobian's python-cloudservers`__. The python-keystoneclient is licensed -under the Apache License like the rest of OpenStack. +Python bindings to the OpenStack Identity API (Keystone) +======================================================== -__ http://github.com/rackspace/python-novaclient -__ http://github.com/jacobian/python-cloudservers +.. image:: https://img.shields.io/pypi/v/python-keystoneclient.svg + :target: https://pypi.org/project/python-keystoneclient/ + :alt: Latest Version + +This is a client for the OpenStack Identity API, implemented by the Keystone +team; it contains a Python API (the ``keystoneclient`` module) for +OpenStack's Identity Service. For command line interface support, use +`OpenStackClient`_. + +* `PyPi`_ - package installation +* `Online Documentation`_ +* `Launchpad project`_ - release management +* `Blueprints`_ - feature specifications +* `Bugs`_ - issue tracking +* `Source`_ +* `Specs`_ +* `How to Contribute`_ +* `Release Notes`_ + +.. _PyPi: https://pypi.org/project/python-keystoneclient +.. _Online Documentation: https://docs.openstack.org/python-keystoneclient/latest/ +.. _Launchpad project: https://launchpad.net/python-keystoneclient +.. _Blueprints: https://blueprints.launchpad.net/python-keystoneclient +.. _Bugs: https://bugs.launchpad.net/python-keystoneclient +.. _Source: https://opendev.org/openstack/python-keystoneclient +.. _OpenStackClient: https://pypi.org/project/python-openstackclient +.. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html +.. _Specs: https://specs.openstack.org/openstack/keystone-specs/ +.. _Release Notes: https://docs.openstack.org/releasenotes/python-keystoneclient .. contents:: Contents: :local: @@ -26,166 +48,15 @@ Python API By way of a quick-start:: - # use v2.0 auth with http://example.com:5000/v2.0 - >>> from keystoneclient.v2_0 import client - >>> keystone = client.Client(username=USERNAME, password=PASSWORD, tenant_name=TENANT, auth_url=AUTH_URL) - >>> keystone.tenants.list() - >>> tenant = keystone.tenants.create(tenant_name="test", description="My new tenant!", enabled=True) - >>> tenant.delete() - - -Command-line API ----------------- - -Installing this package gets you a shell command, ``keystone``, that you can -use to interact with OpenStack's Identity API. - -You'll need to provide your OpenStack tenant, username and password. You can do -this with the ``--os-tenant-name``, ``--os-username`` and ``--os-password`` -params, but it's easier to just set them as environment variables:: - - export OS_TENANT_NAME=project - export OS_USERNAME=user - export OS_PASSWORD=pass - -You will also need to define the authentication url with ``--os-auth-url`` and -the version of the API with ``--os-identity-api-version``. Or set them as an -environment variables as well:: - - export OS_AUTH_URL=http://example.com:5000/v2.0 - export OS_IDENTITY_API_VERSION=2.0 - -Alternatively, to bypass username/password authentication, you can provide a -pre-established token. In Keystone, this approach is necessary to bootstrap the -service with an administrative user, tenant & role (to do so, provide the -client with the value of your ``admin_token`` defined in ``keystone.conf`` in -addition to the URL of your admin API deployment, typically on port 35357):: - - export OS_SERVICE_TOKEN=thequickbrownfox-jumpsover-thelazydog - export OS_SERVICE_ENDPOINT=http://example.com:35357/v2.0 - -Since the Identity service can return multiple regions in the service catalog, -you can specify the one you want with ``--os-region-name`` (or ``export -OS_REGION_NAME``):: - - export OS_REGION_NAME=north - -.. WARNING:: - - If a region is not specified and multiple regions are returned by the - Identity service, the client may not access the same region consistently. - -If you need to connect to a server that is TLS-enabled (the auth URL begins -with 'https') and it uses a certificate from a private CA or a self-signed -certificate you will need to specify the path to an appropriate CA certificate -to use to validate the server certificate with ``--os-cacert`` or an -environment variable:: - - export OS_CACERT=/etc/ssl/my-root-cert.pem - -Certificate verification can be turned off using ``--insecure``. This should -be used with caution. - -You'll find complete documentation on the shell by running ``keystone help``:: - - usage: keystone [--os-username ] - [--os-password ] - [--os-tenant-name ] - [--os-tenant-id ] [--os-auth-url ] - [--os-region-name ] - [--os-identity-api-version ] - [--os-token ] - [--os-endpoint ] - [--os-cacert ] [--insecure] - [--os-cert ] [--os-key ] [--no-cache] - ... - - Command-line interface to the OpenStack Identity API. - - Positional arguments: - - catalog - ec2-credentials-create - Create EC2-compatible credentials for user per tenant - ec2-credentials-delete - Delete EC2-compatible credentials - ec2-credentials-get - Display EC2-compatible credentials - ec2-credentials-list - List EC2-compatible credentials for a user - endpoint-create Create a new endpoint associated with a service - endpoint-delete Delete a service endpoint - endpoint-get - endpoint-list List configured service endpoints - role-create Create new role - role-delete Delete role - role-get Display role details - role-list List all roles - service-create Add service to Service Catalog - service-delete Delete service from Service Catalog - service-get Display service from Service Catalog - service-list List all services in Service Catalog - tenant-create Create new tenant - tenant-delete Delete tenant - tenant-get Display tenant details - tenant-list List all tenants - tenant-update Update tenant name, description, enabled status - token-get - user-create Create new user - user-delete Delete user - user-get Display user details. - user-list List users - user-password-update - Update user password - user-role-add Add role to user - user-role-list List roles granted to a user - user-role-remove Remove role from user - user-update Update user's name, email, and enabled status - discover Discover Keystone servers and show authentication - protocols and - bootstrap Grants a new role to a new user on a new tenant, after - creating each. - bash-completion Prints all of the commands and options to stdout. - help Display help about this program or one of its - subcommands. - - Optional arguments: - --os-username - Name used for authentication with the OpenStack - Identity service. Defaults to env[OS_USERNAME] - --os-password - Password used for authentication with the OpenStack - Identity service. Defaults to env[OS_PASSWORD] - --os-tenant-name - Tenant to request authorization on. Defaults to - env[OS_TENANT_NAME] - --os-tenant-id - Tenant to request authorization on. Defaults to - env[OS_TENANT_ID] - --os-auth-url - Specify the Identity endpoint to use for - authentication. Defaults to env[OS_AUTH_URL] - --os-region-name - Defaults to env[OS_REGION_NAME] - --os-identity-api-version - Defaults to env[OS_IDENTITY_API_VERSION] or 2.0 - --os-token - Specify an existing token to use instead of retrieving - one via authentication (e.g. with username & - password). Defaults to env[OS_SERVICE_TOKEN] - --os-endpoint - Specify an endpoint to use instead of retrieving one - from the service catalog (via authentication). - Defaults to env[OS_SERVICE_ENDPOINT] - --os-cacert - Specify a CA bundle file to use in verifying a TLS - (https) server certificate. Defaults to env[OS_CACERT] - --insecure Explicitly allow keystoneclient 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. - --os-cert - Defaults to env[OS_CERT] - --os-key Defaults to env[OS_KEY] - - See "keystone help COMMAND" for help on a specific command. + >>> from keystoneauth1.identity import v3 + >>> from keystoneauth1 import session + >>> from keystoneclient.v3 import client + >>> auth = v3.Password(auth_url="http://example.com:5000/v3", username="admin", + ... password="password", project_name="admin", + ... user_domain_id="default", project_domain_id="default") + >>> sess = session.Session(auth=auth) + >>> keystone = client.Client(session=sess) + >>> keystone.projects.list() + [...] + >>> project = keystone.projects.create(name="test", description="My new Project!", domain="default", enabled=True) + >>> project.delete() diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index efceab818..000000000 --- a/babel.cfg +++ /dev/null @@ -1 +0,0 @@ -[python: **.py] diff --git a/bindep.txt b/bindep.txt new file mode 100644 index 000000000..fde5b372e --- /dev/null +++ b/bindep.txt @@ -0,0 +1,21 @@ +# This is a cross-platform list tracking distribution packages needed by tests; +# see https://docs.openstack.org/infra/bindep/ for additional information. + +gettext +libssl-dev [platform:dpkg] +openssl-devel [platform:rpm] + +dbus-devel [platform:rpm] +dbus-glib-devel [platform:rpm] +libdbus-1-dev [platform:dpkg] +libdbus-glib-1-dev [platform:dpkg] +libffi-dev [platform:dpkg] +libffi-devel [platform:rpm] +libsasl2-dev [platform:dpkg] +libxml2-dev [platform:dpkg] +libxslt1-dev [platform:dpkg] +python3-all-dev [platform:dpkg] + +cyrus-sasl-devel [platform:rpm] +libxml2-devel [platform:rpm] +python3-devel [platform:rpm] diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 000000000..6c96d9ac2 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,7 @@ +# These are needed for docs generation +openstackdocstheme>=2.2.1 # Apache-2.0 +sphinx>=2.0.0 # BSD +sphinxcontrib-apidoc>=0.2.0 # BSD +reno>=3.1.0 # Apache-2.0 +lxml>=3.4.1 # BSD +fixtures>=3.0.0 # Apache-2.0/BSD diff --git a/doc/source/_theme/layout.html b/doc/source/_theme/layout.html deleted file mode 100644 index 750b78221..000000000 --- a/doc/source/_theme/layout.html +++ /dev/null @@ -1,83 +0,0 @@ -{% extends "basic/layout.html" %} -{% set css_files = css_files + ['_static/tweaks.css'] %} -{% set script_files = script_files + ['_static/jquery.tweet.js'] %} - -{%- macro sidebar() %} - {%- if not embedded %}{% if not theme_nosidebar|tobool %} -
-
- {%- block sidebarlogo %} - {%- if logo %} - - {%- endif %} - {%- endblock %} - {%- block sidebartoc %} - {%- if display_toc %} -

{{ _('Table Of Contents') }}

- {{ toc }} - {%- endif %} - {%- endblock %} - {%- block sidebarrel %} - {%- if prev %} -

{{ _('Previous topic') }}

-

{{ prev.title }}

- {%- endif %} - {%- if next %} -

{{ _('Next topic') }}

-

{{ next.title }}

- {%- endif %} - {%- endblock %} - {%- block sidebarsourcelink %} - {%- if show_source and has_source and sourcename %} -

{{ _('This Page') }}

- - {%- endif %} - {%- endblock %} - {%- if customsidebar %} - {% include customsidebar %} - {%- endif %} - {%- block sidebarsearch %} - {%- if pagename != "search" %} - - - {%- endif %} - {%- endblock %} -
-
- {%- endif %}{% endif %} -{%- endmacro %} - -{% block relbar1 %}{% endblock relbar1 %} - -{% block header %} - -{% endblock %} \ No newline at end of file diff --git a/doc/source/_theme/theme.conf b/doc/source/_theme/theme.conf deleted file mode 100644 index 1cc400446..000000000 --- a/doc/source/_theme/theme.conf +++ /dev/null @@ -1,4 +0,0 @@ -[theme] -inherit = basic -stylesheet = nature.css -pygments_style = tango diff --git a/doc/source/conf.py b/doc/source/conf.py index 948650761..9c984584d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # python-keystoneclient documentation build configuration file, created by # sphinx-quickstart on Sun Dec 6 14:19:25 2009. # @@ -15,6 +13,7 @@ import os import sys + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) @@ -28,15 +27,17 @@ # 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', +extensions = ['sphinxcontrib.apidoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', - 'sphinx.ext.intersphinx'] + 'sphinx.ext.intersphinx', + 'openstackdocstheme', + ] todo_include_todos = True # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +# templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' @@ -48,17 +49,7 @@ master_doc = 'index' # General information about the project. -project = u'python-keystoneclient' -copyright = u'Rackspace, based on work by Jacob Kaplan-Moss' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short XXXX.Y version. -version = '2012.3' -# The full version, including alpha/beta/rc tags. -release = '2012.3-dev' +copyright = 'OpenStack Contributors' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -93,18 +84,23 @@ #show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'native' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +modindex_common_prefix = ['keystoneclient.'] + +# 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_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 @@ -133,12 +129,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1" -html_last_updated_fmt = os.popen(git_cmd).read() +#html_static_path = ['static'] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. @@ -174,6 +165,14 @@ # Output file base name for HTML help builder. htmlhelp_basename = 'python-keystoneclientdoc' +# -- sphinxcontrib.apidoc configuration -------------------------------------- + +apidoc_module_dir = '../../keystoneclient' +apidoc_output_dir = 'api' +apidoc_excluded_paths = [ + 'fixture', + 'tests', +] # -- Options for LaTeX output ------------------------------------------------- @@ -187,10 +186,9 @@ # (source start file, target name, title, author, documentclass [howto/manual]) # . latex_documents = [ - ('index', 'python-keystoneclient.tex', - u'python-keystoneclient Documentation', - u'Nebula Inc, based on work by Rackspace and Jacob Kaplan-Moss', - 'manual'), + ('index', 'doc-python-keystoneclient.tex', + 'python-keystoneclient Documentation', + 'OpenStack', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -210,9 +208,27 @@ # If false, no module index is generated. #latex_use_modindex = True - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('http://docs.python.org/', None), - 'nova': ('http://nova.openstack.org', None), - 'swift': ('http://swift.openstack.org', None), - 'glance': ('http://glance.openstack.org', None)} +# Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 +latex_use_xindy = False + +latex_domain_indices = False + +latex_elements = { + 'makeindex': '', + 'printindex': '', + 'preamble': r'\setcounter{tocdepth}{3}', + 'maxlistdepth': 10, +} + +keystoneauth_url = 'https://docs.openstack.org/keystoneauth/latest/' +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'osloconfig': ('https://docs.openstack.org/oslo.config/latest/', None), + 'keystoneauth1': (keystoneauth_url, None), +} + +# -- Options for openstackdocstheme ------------------------------------------- +openstackdocs_repo_name = 'openstack/python-keystoneclient' +openstackdocs_bug_project = 'python-keystoneclient' +openstackdocs_bug_tag = '' +openstackdocs_pdf_link = True diff --git a/doc/source/images/graphs_authComp.svg b/doc/source/images/graphs_authComp.svg deleted file mode 100644 index 6be629c12..000000000 --- a/doc/source/images/graphs_authComp.svg +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - -AuthComp - - -AuthComp - -Auth -Component - - - -AuthComp->Reject - - -Reject -Unauthenticated -Requests - - -Service - -OpenStack -Service - - -AuthComp->Service - - -Forward -Authenticated -Requests - - - -Start->AuthComp - - - - - diff --git a/doc/source/images/graphs_authCompDelegate.svg b/doc/source/images/graphs_authCompDelegate.svg deleted file mode 100644 index 4788829a4..000000000 --- a/doc/source/images/graphs_authCompDelegate.svg +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - -AuthCompDelegate - - -AuthComp - -Auth -Component - - - -AuthComp->Reject - - -Reject Requests -Indicated by the Service - - -Service - -OpenStack -Service - - -AuthComp->Service - - -Forward Requests -with Identiy Status - - -Service->AuthComp - - -Send Response OR -Reject Message - - - -Start->AuthComp - - - - - diff --git a/doc/source/index.rst b/doc/source/index.rst index faffc4f36..f1114b67c 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,33 +1,50 @@ +======================================================== Python bindings to the OpenStack Identity API (Keystone) ======================================================== -This is a client for OpenStack Identity API. There's :doc:`a Python API -` (the :mod:`keystoneclient` module), and a :doc:`command-line script -` (installed as :program:`keystone`). +This is a client for OpenStack Identity API. There's a Python API for +:doc:`Identity API v3 ` and :doc:`v2 ` (the +:mod:`keystoneclient` modules). Contents: .. toctree:: :maxdepth: 1 - releases - shell - using-api + using-api-v3 + using-sessions + using-api-v2 + api/modules + +Related Identity Projects +========================= + +In addition to creating the Python client library, the Keystone team also +provides `Identity Service`_, as well as `WSGI Middleware`_. + +.. _`Identity Service`: https://docs.openstack.org/keystone/latest/ +.. _`WSGI Middleware`: https://docs.openstack.org/keystonemiddleware/latest/ + +Release Notes +============= + + +Read also the `Keystoneclient Release Notes +`_. - api/autoindex Contributing ============ -Code is hosted `on GitHub`_. Submit bugs to the Keystone project on +Code is hosted `on OpenDev`_. Submit bugs to the Keystone project on `Launchpad`_. Submit code to the ``openstack/python-keystoneclient`` project using `Gerrit`_. -.. _on GitHub: https://github.com/openstack/python-keystoneclient +.. _on OpenDev: https://opendev.org/openstack/python-keystoneclient .. _Launchpad: https://launchpad.net/python-keystoneclient -.. _Gerrit: http://wiki.openstack.org/GerritWorkflow +.. _Gerrit: https://docs.openstack.org/infra/manual/developers.html#development-workflow -Run tests with ``python setup.py test``. +Run tests with ``tox``. Indices and tables ================== diff --git a/doc/source/middlewarearchitecture.rst b/doc/source/middlewarearchitecture.rst deleted file mode 100644 index 59a6db025..000000000 --- a/doc/source/middlewarearchitecture.rst +++ /dev/null @@ -1,309 +0,0 @@ -.. - Copyright 2011-2012 OpenStack, LLC - All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); you may - not use this file except in compliance with the License. You may obtain - a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - License for the specific language governing permissions and limitations - under the License. - -======================= -Middleware Architecture -======================= - -Abstract -======== - -The Keystone middleware architecture supports a common authentication protocol -in use between the OpenStack projects. By using keystone as a common -authentication and authorization mechanisms, the OpenStack project can plug in -to existing authentication and authorization systems in use by existing -environments. - -In this document, we describe the architecture and responsibilities of the -authentication middleware which acts as the internal API mechanism for -OpenStack projects based on the WSGI standard. - -For the architecture of keystone and its services, please see -:doc:`architecture`. This documentation primarily describes the implementation -in ``keystoneclient/middleware/auth_token.py`` -(:py:class:`keystoneclient.middleware.auth_token.AuthProtocol`) - -Specification Overview -====================== - -'Authentication' is the process of determining that users are who they say they -are. Typically, 'authentication protocols' such as HTTP Basic Auth, Digest -Access, public key, token, etc, are used to verify a user's identity. In this -document, we define an ''authentication component'' as a software module that -implements an authentication protocol for an OpenStack service. OpenStack is -using a token based mechanism to represent authentication and authorization. - -At a high level, an authentication middleware component is a proxy that -intercepts HTTP calls from clients and populates HTTP headers in the request -context for other WSGI middleware or applications to use. The general flow -of the middleware processing is: - -* clear any existing authorization headers to prevent forgery -* collect the token from the existing HTTP request headers -* validate the token - - * if valid, populate additional headers representing the identity that has - been authenticated and authorized - * in invalid, or not token present, reject the request (HTTPUnauthorized) - or pass along a header indicating the request is unauthorized (configurable - in the middleware) - * if the keystone service is unavailable to validate the token, reject - the request with HTTPServiceUnavailable. - -.. _authComponent: - -Authentication Component ------------------------- - -Figure 1. Authentication Component - -.. image:: images/graphs_authComp.svg - :width: 100% - :height: 180 - :alt: An Authentication Component - -The middleware may also be configured to operated in a 'delegated mode'. -In this mode, the decision reject an unauthenticated client is delegated to -the OpenStack service, as illustrated in :ref:`authComponentDelegated`. - -Here, requests are forwarded to the OpenStack service with an identity status -message that indicates whether the client's identity has been confirmed or is -indeterminate. It is the OpenStack service that decides whether or not a reject -message should be sent to the client. - -.. _authComponentDelegated: - -Authentication Component (Delegated Mode) ------------------------------------------ - -Figure 2. Authentication Component (Delegated Mode) - -.. image:: images/graphs_authCompDelegate.svg - :width: 100% - :height: 180 - :alt: An Authentication Component (Delegated Mode) - -.. _deployStrategies: - -Deployment Strategy -=================== - -The middleware is intended to be used inline with OpenStack wsgi components, -based on the openstack-common WSGI middleware class. It is typically deployed -as a configuration element in a paste configuration pipeline of other -middleware components, with the pipeline terminating in the service -application. The middleware conforms to the python WSGI standard [PEP-333]_. -In initializing the middleware, a configuration item (which acts like a python -dictionary) is passed to the middleware with relevant configuration options. - -Configuration -------------- - -The middleware is configured within the config file of the main application as -a WSGI component. Example for the auth_token middleware:: - - [app:myService] - paste.app_factory = myService:app_factory - - [pipeline:main] - pipeline = tokenauth myService - - [filter:tokenauth] - paste.filter_factory = keystone.middleware.auth_token:filter_factory - auth_host = 127.0.0.1 - auth_port = 35357 - auth_protocol = http - auth_uri = http://127.0.0.1:5000/ - admin_token = Super999Sekret888Password777 - admin_user = admin - admin_password = SuperSekretPassword - admin_tenant_name = service - ;Uncomment next line to use Swift MemcacheRing - ;cache = swift.cache - ;Uncomment next line and check ip:port to use memcached to cache tokens - ;memcache_servers = 127.0.0.1:11211 - ;Uncomment next 2 lines to turn on memcache protection - ;memcache_security_strategy = ENCRYPT - ;memcache_secret_key = change_me - ;Uncomment next 2 lines if Keystone server is validating client cert - ;certfile = - ;keyfile = - -For services which have separate paste-deploy ini file, auth_token middleware -can be alternatively configured in [keystone_authtoken] section in the main -config file. For example in Nova, all middleware parameters can be removed -from api-paste.ini:: - - [filter:authtoken] - paste.filter_factory = keystone.middleware.auth_token:filter_factory - -and set in nova.conf:: - - [DEFAULT] - ... - auth_strategy=keystone - - [keystone_authtoken] - auth_host = 127.0.0.1 - auth_port = 35357 - auth_protocol = http - auth_uri = http://127.0.0.1:5000/ - admin_user = admin - admin_password = SuperSekretPassword - admin_tenant_name = service - -Note that middleware parameters in paste config take priority, they must be -removed to use values in [keystone_authtoken] section. - -Configuration Options ---------------------- - -* ``auth_host``: (required) the host providing the keystone service API endpoint - for validating and requesting tokens -* ``admin_token``: either this or the following three options are required. If - set, this is a single shared secret with the keystone configuration used to - validate tokens. -* ``admin_user``, ``admin_password``, ``admin_tenant_name``: if ``admin_token`` - is not set, or invalid, then admin_user, admin_password, and - admin_tenant_name are defined as a service account which is expected to have - been previously configured in Keystone to validate user tokens. - -* ``delay_auth_decision``: (optional, default `0`) (off). If on, the middleware - will not reject invalid auth requests, but will delegate that decision to - downstream WSGI components. -* ``auth_port``: (optional, default `35357`) the port used to validate tokens -* ``auth_protocol``: (optional, default `https`) -* ``auth_uri``: (optional, defaults to `auth_protocol`://`auth_host`:`auth_port`) -* ``certfile``: (required, if Keystone server requires client cert) -* ``keyfile``: (required, if Keystone server requires client cert) This can be - the same as the certfile if the certfile includes the private key. - -Caching for improved response ------------------------------ - -In order to prevent every service request, the middleware may be configured -to utilize a cache, and the keystone API returns the tokens with an -expiration (configurable in duration on the keystone service). The middleware -supports memcache based caching. - -* ``memcache_servers``: (optonal) if defined, the memcache server(s) to use for - cacheing. It will be ignored if Swift MemcacheRing is used instead. -* ``token_cache_time``: (optional, default 300 seconds) Only valid if - memcache_servers is defined. - -When deploying auth_token middleware with Swift, user may elect -to use Swift MemcacheRing instead of the local Keystone memcache. -The Swift MemcacheRing object is passed in from the request environment -and it defaults to 'swift.cache'. However it could be -different, depending on deployment. To use Swift MemcacheRing, you must -provide the ``cache`` option. - -* ``cache``: (optional) if defined, the environment key where the Swift - MemcacheRing object is stored. - -Memcached and System Time -========================= - -When using `memcached`_ with ``auth_token`` middleware, ensure that the system -time of memcached hosts is set to UTC. Memcached uses the host's system -time in determining whether a key has expired, whereas Keystone sets -key expiry in UTC. The timezone used by Keystone and memcached must -match if key expiry is to behave as expected. - -.. _`memcached`: http://memcached.org/ - -Memcache Protection -=================== - -When using memcached, we are storing user tokens and token validation -information into the cache as raw data. Which means anyone who have access -to the memcache servers can read and modify data stored there. To mitigate -this risk, ``auth_token`` middleware provides an option to either encrypt -or authenticate the token data stored in the cache. - -* ``memcache_security_strategy``: (optional) if defined, indicate whether token - data should be encrypted or authenticated. Acceptable values are ``ENCRYPT`` - or ``MAC``. If ``ENCRYPT``, token data is encrypted in the cache. If - ``MAC``, token data is authenticated (with HMAC) in the cache. If its value - is neither ``MAC`` nor ``ENCRYPT``, ``auth_token`` will raise an exception - on initialization. -* ``memcache_secret_key``: (optional, mandatory if - ``memcache_security_strategy`` is defined) if defined, - a random string to be used for key derivation. If - ``memcache_security_strategy`` is defined and ``memcache_secret_key`` is - absent, ``auth_token`` will raise an exception on initialization. - -Exchanging User Information -=========================== - -The middleware expects to find a token representing the user with the header -``X-Auth-Token`` or ``X-Storage-Token``. `X-Storage-Token` is supported for -swift/cloud files and for legacy Rackspace use. If the token isn't present and -the middleware is configured to not delegate auth responsibility, it will -respond to the HTTP request with HTTPUnauthorized, returning the header -``WWW-Authenticate`` with the value `Keystone uri='...'` to indicate where to -request a token. The auth_uri returned is configured with the middleware. - -The authentication middleware extends the HTTP request with the header -``X-Identity-Status``. If a request is successfully authenticated, the value -is set to `Confirmed`. If the middleware is delegating the auth decision to the -service, then the status is set to `Invalid` if the auth request was -unsuccessful. - -Extended the request with additional User Information ------------------------------------------------------ - -:py:class:`keystone.middleware.auth_token.AuthProtocol` extends the request -with additional information if the user has been authenticated. - - -X-Identity-Status - Provides information on whether the request was authenticated or not. - -X-Tenant-Id - The unique, immutable tenant Id - -X-Tenant-Name - The unique, but mutable (it can change) tenant name. - -X-User-Id - The user id of the user used to log in - -X-User-Name - The username used to log in - -X-Roles - The roles associated with that user - -Deprecated additions --------------------- - -X-Tenant - Provides the tenant name. This is to support any legacy implementations - before Keystone switched to an ID/Name schema for tenants. - -X-User - The username used to log in. This is to support any legacy implementations - before Keystone switched to an ID/Name schema for tenants. - -X-Role - The roles associated with that user - -References -========== - -.. [PEP-333] pep0333 Phillip J Eby. 'Python Web Server Gateway Interface - v1.0.'' http://www.python.org/dev/peps/pep-0333/. diff --git a/doc/source/releases.rst b/doc/source/releases.rst deleted file mode 100644 index bb3f1a0c8..000000000 --- a/doc/source/releases.rst +++ /dev/null @@ -1,33 +0,0 @@ -============= -Release notes -============= - -0.1.3 (August 31, 2012) -======================= -* changed logging to report request and response independently in --debug mode -* changed options to use hyphens instead of underscores -* added support for PKI signed tokens with Keystone - - -0.1.2 (July 9, 2012) -==================== -* added support for two-way SSL and --insecure option to allow for self-signed - certificates -* added support for password prompting if not provided -* added support for bash completion for keystone -* updated CLI options to use dashes instead of underscores - -0.1.1 (June 25, 2012) -===================== -* corrected versioning - -0.1.0 (March 29, 2012) -====================== -* released with OpenStack Essex and Diablo compatibility -* forked from http://github.com/rackspace/python-novaclient -* refactored to support Identity API (auth, tokens, services, roles, tenants, - users, etc.) -* removed legacy arguments of --username, --password, etc in migration to - support a cross-openstack unified CLI convention defined at - http://wiki.openstack.org/UnifiedCLI -* required service ID for listing endpoints diff --git a/doc/source/shell.rst b/doc/source/shell.rst deleted file mode 100644 index 02d158c88..000000000 --- a/doc/source/shell.rst +++ /dev/null @@ -1,99 +0,0 @@ -The :program:`keystone` shell utility -===================================== - -.. program:: keystone -.. highlight:: bash - - -The :program:`keystone` shell utility interacts with OpenStack Identity API -from the command line. It supports the entirety of the OpenStack Identity API. - -To communicate with the API, you will need to be authenticated - and the -:program:`keystone` provides multiple options for this. - -While bootstrapping keystone the authentication is accomplished with a -shared secret token and the location of the Identity API endpoint. The -shared secret token is configured in keystone.conf as "admin_token". - -You can specify those values on the command line with :option:`--os-token` -and :option:`--os-endpoint`, or set them in environment variables: - -.. envvar:: OS_SERVICE_TOKEN - - Your keystone administrative token - -.. envvar:: OS_SERVICE_ENDPOINT - - Your Identity API endpoint - -The command line options will override any environment variables set. - -If you already have accounts, you can use your OpenStack username and -password. You can do this with the :option:`--os-username`, -:option:`--os-password`. - -Keystone allows a user to be associated with one or more tenants. To specify -the tenant for which you want to authorize against, you may optionally -specify a :option:`--os-tenant-id` or :option:`--os-tenant-name`. - -Instead of using options, it is easier to just set them as environment -variables: - -.. envvar:: OS_USERNAME - - Your Keystone username. - -.. envvar:: OS_PASSWORD - - Your Keystone password. - -.. envvar:: OS_TENANT_NAME - - Name of Keystone Tenant. - -.. envvar:: OS_TENANT_ID - - ID of Keystone Tenant. - -.. envvar:: OS_AUTH_URL - - The OpenStack API server URL. - -.. envvar:: OS_IDENTITY_API_VERSION - - The OpenStack Identity API version. - -.. envvar:: OS_CACERT - - The location for the CA truststore (PEM formatted) for this client. - -.. envvar:: OS_CERT - - The location for the keystore (PEM formatted) containing the public - key of this client. This keystore can also optionally contain the - private key of this client. - -.. envvar:: OS_KEY - - The location for the keystore (PEM formatted) containing the private - key of this client. This value can be empty if the private key is - included in the OS_CERT file. - -For example, in Bash you'd use:: - - export OS_USERNAME=yourname - export OS_PASSWORD=yadayadayada - export OS_TENANT_NAME=myproject - export OS_AUTH_URL=http(s)://example.com:5000/v2.0/ - export OS_IDENTITY_API_VERSION=2.0 - export OS_CACERT=/etc/keystone/yourca.pem - export OS_CERT=/etc/keystone/yourpublickey.pem - export OS_KEY=/etc/keystone/yourprivatekey.pem - -From there, all shell commands take the form:: - - keystone [arguments...] - -Run :program:`keystone help` to get a full list of all possible commands, -and run :program:`keystone help ` to get detailed help for that -command. diff --git a/doc/source/static/basic.css b/doc/source/static/basic.css deleted file mode 100644 index d909ce37c..000000000 --- a/doc/source/static/basic.css +++ /dev/null @@ -1,416 +0,0 @@ -/** - * Sphinx stylesheet -- basic theme - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -img { - border: 0; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li div.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable dl, table.indextable dd { - margin-top: 0; - margin-bottom: 0; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -/* -- general body styles --------------------------------------------------- */ - -a.headerlink { - visibility: hidden; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.field-list ul { - padding-left: 1em; -} - -.first { -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px 7px 0 7px; - background-color: #ffe; - width: 40%; - float: right; -} - -p.sidebar-title { - font-weight: bold; -} - -/* -- topics ---------------------------------------------------------------- */ - -div.topic { - border: 1px solid #ccc; - padding: 7px 7px 0 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -div.admonition dl { - margin-bottom: 0; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - border: 0; - border-collapse: collapse; -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 0; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -table.field-list td, table.field-list th { - border: 0 !important; -} - -table.footnote td, table.footnote th { - border: 0 !important; -} - -th { - text-align: left; - padding-right: 5px; -} - -/* -- other body styles ----------------------------------------------------- */ - -dl { - margin-bottom: 15px; -} - -dd p { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -dt:target, .highlight { - background-color: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.refcount { - color: #060; -} - -.optional { - font-size: 1.3em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; -} - -td.linenos pre { - padding: 5px 0px; - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - margin-left: 0.5em; -} - -table.highlighttable td { - padding: 0 0.5em 0 0.5em; -} - -tt.descname { - background-color: transparent; - font-weight: bold; - font-size: 1.2em; -} - -tt.descclassname { - background-color: transparent; -} - -tt.xref, a tt { - background-color: transparent; - font-weight: bold; -} - -h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { - background-color: transparent; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} diff --git a/doc/source/static/default.css b/doc/source/static/default.css deleted file mode 100644 index c8091ecb4..000000000 --- a/doc/source/static/default.css +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Sphinx stylesheet -- default theme - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: sans-serif; - font-size: 100%; - background-color: #11303d; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - background-color: #1c4e63; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 230px; -} - -div.body { - background-color: #ffffff; - color: #000000; - padding: 0 20px 30px 20px; -} - -div.footer { - color: #ffffff; - width: 100%; - padding: 9px 0 9px 0; - text-align: center; - font-size: 75%; -} - -div.footer a { - color: #ffffff; - text-decoration: underline; -} - -div.related { - background-color: #133f52; - line-height: 30px; - color: #ffffff; -} - -div.related a { - color: #ffffff; -} - -div.sphinxsidebar { -} - -div.sphinxsidebar h3 { - font-family: 'Trebuchet MS', sans-serif; - color: #ffffff; - font-size: 1.4em; - font-weight: normal; - margin: 0; - padding: 0; -} - -div.sphinxsidebar h3 a { - color: #ffffff; -} - -div.sphinxsidebar h4 { - font-family: 'Trebuchet MS', sans-serif; - color: #ffffff; - font-size: 1.3em; - font-weight: normal; - margin: 5px 0 0 0; - padding: 0; -} - -div.sphinxsidebar p { - color: #ffffff; -} - -div.sphinxsidebar p.topless { - margin: 5px 10px 10px 10px; -} - -div.sphinxsidebar ul { - margin: 10px; - padding: 0; - color: #ffffff; -} - -div.sphinxsidebar a { - color: #98dbcc; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #355f7c; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -div.body p, div.body dd, div.body li { - text-align: left; - line-height: 130%; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Trebuchet MS', sans-serif; - background-color: #f2f2f2; - font-weight: normal; - color: #20435c; - border-bottom: 1px solid #ccc; - margin: 20px -20px 10px -20px; - padding: 3px 0 3px 10px; -} - -div.body h1 { margin-top: 0; font-size: 200%; } -div.body h2 { font-size: 160%; } -div.body h3 { font-size: 140%; } -div.body h4 { font-size: 120%; } -div.body h5 { font-size: 110%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #c60f0f; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: #c60f0f; - color: white; -} - -div.body p, div.body dd, div.body li { - text-align: left; - line-height: 130%; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.admonition p { - margin-bottom: 5px; -} - -div.admonition pre { - margin-bottom: 5px; -} - -div.admonition ul, div.admonition ol { - margin-bottom: 5px; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre { - padding: 5px; - background-color: #eeffcc; - color: #333333; - line-height: 120%; - border: 1px solid #ac9; - border-left: none; - border-right: none; -} - -tt { - background-color: #ecf0f3; - padding: 0 1px 0 1px; - font-size: 0.95em; -} - -.warning tt { - background: #efc2c2; -} - -.note tt { - background: #d6d6d6; -} diff --git a/doc/source/static/header-line.gif b/doc/source/static/header-line.gif deleted file mode 100644 index 3601730e0..000000000 Binary files a/doc/source/static/header-line.gif and /dev/null differ diff --git a/doc/source/static/header_bg.jpg b/doc/source/static/header_bg.jpg deleted file mode 100644 index f788c41c2..000000000 Binary files a/doc/source/static/header_bg.jpg and /dev/null differ diff --git a/doc/source/static/jquery.tweet.js b/doc/source/static/jquery.tweet.js deleted file mode 100644 index 79bf0bdb4..000000000 --- a/doc/source/static/jquery.tweet.js +++ /dev/null @@ -1,154 +0,0 @@ -(function($) { - - $.fn.tweet = function(o){ - var s = { - username: ["seaofclouds"], // [string] required, unless you want to display our tweets. :) it can be an array, just do ["username1","username2","etc"] - list: null, //[string] optional name of list belonging to username - avatar_size: null, // [integer] height and width of avatar if displayed (48px max) - count: 3, // [integer] how many tweets to display? - intro_text: null, // [string] do you want text BEFORE your your tweets? - outro_text: null, // [string] do you want text AFTER your tweets? - join_text: null, // [string] optional text in between date and tweet, try setting to "auto" - auto_join_text_default: "i said,", // [string] auto text for non verb: "i said" bullocks - auto_join_text_ed: "i", // [string] auto text for past tense: "i" surfed - auto_join_text_ing: "i am", // [string] auto tense for present tense: "i was" surfing - auto_join_text_reply: "i replied to", // [string] auto tense for replies: "i replied to" @someone "with" - auto_join_text_url: "i was looking at", // [string] auto tense for urls: "i was looking at" http:... - loading_text: null, // [string] optional loading text, displayed while tweets load - query: null // [string] optional search query - }; - - if(o) $.extend(s, o); - - $.fn.extend({ - linkUrl: function() { - var returning = []; - var regexp = /((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi; - this.each(function() { - returning.push(this.replace(regexp,"$1")); - }); - return $(returning); - }, - linkUser: function() { - var returning = []; - var regexp = /[\@]+([A-Za-z0-9-_]+)/gi; - this.each(function() { - returning.push(this.replace(regexp,"@$1")); - }); - return $(returning); - }, - linkHash: function() { - var returning = []; - var regexp = / [\#]+([A-Za-z0-9-_]+)/gi; - this.each(function() { - returning.push(this.replace(regexp, ' #$1')); - }); - return $(returning); - }, - capAwesome: function() { - var returning = []; - this.each(function() { - returning.push(this.replace(/\b(awesome)\b/gi, '$1')); - }); - return $(returning); - }, - capEpic: function() { - var returning = []; - this.each(function() { - returning.push(this.replace(/\b(epic)\b/gi, '$1')); - }); - return $(returning); - }, - makeHeart: function() { - var returning = []; - this.each(function() { - returning.push(this.replace(/(<)+[3]/gi, "")); - }); - return $(returning); - } - }); - - function relative_time(time_value) { - var parsed_date = Date.parse(time_value); - var relative_to = (arguments.length > 1) ? arguments[1] : new Date(); - var delta = parseInt((relative_to.getTime() - parsed_date) / 1000); - var pluralize = function (singular, n) { - return '' + n + ' ' + singular + (n == 1 ? '' : 's'); - }; - if(delta < 60) { - return 'less than a minute ago'; - } else if(delta < (45*60)) { - return 'about ' + pluralize("minute", parseInt(delta / 60)) + ' ago'; - } else if(delta < (24*60*60)) { - return 'about ' + pluralize("hour", parseInt(delta / 3600)) + ' ago'; - } else { - return 'about ' + pluralize("day", parseInt(delta / 86400)) + ' ago'; - } - } - - function build_url() { - var proto = ('https:' == document.location.protocol ? 'https:' : 'http:'); - if (s.list) { - return proto+"//api.twitter.com/1/"+s.username[0]+"/lists/"+s.list+"/statuses.json?per_page="+s.count+"&callback=?"; - } else if (s.query == null && s.username.length == 1) { - return proto+'//twitter.com/status/user_timeline/'+s.username[0]+'.json?count='+s.count+'&callback=?'; - } else { - var query = (s.query || 'from:'+s.username.join('%20OR%20from:')); - return proto+'//search.twitter.com/search.json?&q='+query+'&rpp='+s.count+'&callback=?'; - } - } - - return this.each(function(){ - var list = $('
    ').appendTo(this); - var intro = '

    '+s.intro_text+'

    '; - var outro = '

    '+s.outro_text+'

    '; - var loading = $('

    '+s.loading_text+'

    '); - - if(typeof(s.username) == "string"){ - s.username = [s.username]; - } - - if (s.loading_text) $(this).append(loading); - $.getJSON(build_url(), function(data){ - if (s.loading_text) loading.remove(); - if (s.intro_text) list.before(intro); - $.each((data.results || data), function(i,item){ - // auto join text based on verb tense and content - if (s.join_text == "auto") { - if (item.text.match(/^(@([A-Za-z0-9-_]+)) .*/i)) { - var join_text = s.auto_join_text_reply; - } else if (item.text.match(/(^\w+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+) .*/i)) { - var join_text = s.auto_join_text_url; - } else if (item.text.match(/^((\w+ed)|just) .*/im)) { - var join_text = s.auto_join_text_ed; - } else if (item.text.match(/^(\w*ing) .*/i)) { - var join_text = s.auto_join_text_ing; - } else { - var join_text = s.auto_join_text_default; - } - } else { - var join_text = s.join_text; - }; - - var from_user = item.from_user || item.user.screen_name; - var profile_image_url = item.profile_image_url || item.user.profile_image_url; - var join_template = ' '+join_text+' '; - var join = ((s.join_text) ? join_template : ' '); - var avatar_template = ''+from_user+'\'s avatar'; - var avatar = (s.avatar_size ? avatar_template : ''); - var date = ''+relative_time(item.created_at)+''; - var text = '' +$([item.text]).linkUrl().linkUser().linkHash().makeHeart().capAwesome().capEpic()[0]+ ''; - - // until we create a template option, arrange the items below to alter a tweet's display. - list.append('
  • ' + avatar + date + join + text + '
  • '); - - list.children('li:first').addClass('tweet_first'); - list.children('li:odd').addClass('tweet_even'); - list.children('li:even').addClass('tweet_odd'); - }); - if (s.outro_text) list.after(outro); - }); - - }); - }; -})(jQuery); \ No newline at end of file diff --git a/doc/source/static/nature.css b/doc/source/static/nature.css deleted file mode 100644 index a98bd4209..000000000 --- a/doc/source/static/nature.css +++ /dev/null @@ -1,245 +0,0 @@ -/* - * nature.css_t - * ~~~~~~~~~~~~ - * - * Sphinx stylesheet -- nature theme. - * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: Arial, sans-serif; - font-size: 100%; - background-color: #111; - color: #555; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ theme_sidebarwidth|toint }}px; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.document { - background-color: #eee; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; - font-size: 0.9em; -} - -div.footer { - color: #555; - width: 100%; - padding: 13px 0; - text-align: center; - font-size: 75%; -} - -div.footer a { - color: #444; - text-decoration: underline; -} - -div.related { - background-color: #6BA81E; - line-height: 32px; - color: #fff; - text-shadow: 0px 1px 0 #444; - font-size: 0.9em; -} - -div.related a { - color: #E2F3CC; -} - -div.sphinxsidebar { - font-size: 0.75em; - line-height: 1.5em; -} - -div.sphinxsidebarwrapper{ - padding: 20px 0; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: Arial, sans-serif; - color: #222; - font-size: 1.2em; - font-weight: normal; - margin: 0; - padding: 5px 10px; - background-color: #ddd; - text-shadow: 1px 1px 0 white -} - -div.sphinxsidebar h4{ - font-size: 1.1em; -} - -div.sphinxsidebar h3 a { - color: #444; -} - - -div.sphinxsidebar p { - color: #888; - padding: 5px 20px; -} - -div.sphinxsidebar p.topless { -} - -div.sphinxsidebar ul { - margin: 10px 20px; - padding: 0; - color: #000; -} - -div.sphinxsidebar a { - color: #444; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar input[type=text]{ - margin-left: 20px; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #005B81; - text-decoration: none; -} - -a:hover { - color: #E32E00; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: Arial, sans-serif; - background-color: #BED4EB; - font-weight: normal; - color: #212224; - margin: 30px 0px 10px 0px; - padding: 5px 0 5px 10px; - text-shadow: 0px 1px 0 white -} - -div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } -div.body h2 { font-size: 150%; background-color: #C8D5E3; } -div.body h3 { font-size: 120%; background-color: #D8DEE3; } -div.body h4 { font-size: 110%; background-color: #D8DEE3; } -div.body h5 { font-size: 100%; background-color: #D8DEE3; } -div.body h6 { font-size: 100%; background-color: #D8DEE3; } - -a.headerlink { - color: #c60f0f; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: #c60f0f; - color: white; -} - -div.body p, div.body dd, div.body li { - line-height: 1.5em; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.highlight{ - background-color: white; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre { - padding: 10px; - background-color: White; - color: #222; - line-height: 1.2em; - border: 1px solid #C6C9CB; - font-size: 1.1em; - margin: 1.5em 0 1.5em 0; - -webkit-box-shadow: 1px 1px 1px #d8d8d8; - -moz-box-shadow: 1px 1px 1px #d8d8d8; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ - font-size: 1.1em; - font-family: monospace; -} - -.viewcode-back { - font-family: Arial, sans-serif; -} - -div.viewcode-block:target { - background-color: #f4debf; - border-top: 1px solid #ac9; - border-bottom: 1px solid #ac9; -} diff --git a/doc/source/static/openstack_logo.png b/doc/source/static/openstack_logo.png deleted file mode 100644 index 146faec5c..000000000 Binary files a/doc/source/static/openstack_logo.png and /dev/null differ diff --git a/doc/source/static/tweaks.css b/doc/source/static/tweaks.css deleted file mode 100644 index 3f3fb3f07..000000000 --- a/doc/source/static/tweaks.css +++ /dev/null @@ -1,94 +0,0 @@ -body { - background: #fff url(../_static/header_bg.jpg) top left no-repeat; -} - -#header { - width: 950px; - margin: 0 auto; - height: 102px; -} - -#header h1#logo { - background: url(../_static/openstack_logo.png) top left no-repeat; - display: block; - float: left; - text-indent: -9999px; - width: 175px; - height: 55px; -} - -#navigation { - background: url(../_static/header-line.gif) repeat-x 0 bottom; - display: block; - float: left; - margin: 27px 0 0 25px; - padding: 0; -} - -#navigation li{ - float: left; - display: block; - margin-right: 25px; -} - -#navigation li a { - display: block; - font-weight: normal; - text-decoration: none; - background-position: 50% 0; - padding: 20px 0 5px; - color: #353535; - font-size: 14px; -} - -#navigation li a.current, #navigation li a.section { - border-bottom: 3px solid #cf2f19; - color: #cf2f19; -} - -div.related { - background-color: #cde2f8; - border: 1px solid #b0d3f8; -} - -div.related a { - color: #4078ba; - text-shadow: none; -} - -div.sphinxsidebarwrapper { - padding-top: 0; -} - -pre { - color: #555; -} - -div.documentwrapper h1, div.documentwrapper h2, div.documentwrapper h3, div.documentwrapper h4, div.documentwrapper h5, div.documentwrapper h6 { - font-family: 'PT Sans', sans-serif !important; - color: #264D69; - border-bottom: 1px dotted #C5E2EA; - padding: 0; - background: none; - padding-bottom: 5px; -} - -div.documentwrapper h3 { - color: #CF2F19; -} - -a.headerlink { - color: #fff !important; - margin-left: 5px; - background: #CF2F19 !important; -} - -div.body { - margin-top: -25px; - margin-left: 230px; -} - -div.document { - width: 960px; - margin: 0 auto; -} \ No newline at end of file diff --git a/doc/source/using-api.rst b/doc/source/using-api-v2.rst similarity index 68% rename from doc/source/using-api.rst rename to doc/source/using-api-v2.rst index b5d630c12..7b0815f49 100644 --- a/doc/source/using-api.rst +++ b/doc/source/using-api-v2.rst @@ -1,11 +1,11 @@ -============== -The client API -============== +======================= +Using the V2 client API +======================= Introduction ============ -The main concepts in the Identity API are: +The main concepts in the Identity v2 API are: * tenants * users @@ -13,11 +13,11 @@ The main concepts in the Identity API are: * services * endpoints -The Identity API lets you query and make changes through managers. For example, -to manipulate tenants, you interact with a +The V2 client API lets you query and make changes through +managers. For example, to manipulate tenants, you interact with a ``keystoneclient.v2_0.tenants.TenantManager`` object. -You obtain access to managers through via attributes of the +You obtain access to managers via attributes of the ``keystoneclient.v2_0.client.Client`` object. For example, the ``tenants`` attribute of the ``Client`` class is a tenant manager:: @@ -26,13 +26,17 @@ attribute of the ``Client`` class is a tenant manager:: >>> keystone.tenants.list() # List tenants You create a valid ``keystoneclient.v2_0.client.Client`` object by passing -authentication data to the constructor. Authentication and examples of common -tasks are provided below. +a :class:`~keystoneauth1.session.Session` to the constructor. Authentication +and examples of common tasks are provided below. + +You can generally expect that when the client needs to propagate an exception +it will raise an instance of subclass of +``keystoneclient.exceptions.ClientException`` Authenticating ============== -There are two ways to authenticate against Keystone: +There are two ways to authenticate against keystone: * against the admin endpoint with the admin token * against the public endpoint with a username and password @@ -41,39 +45,47 @@ endpoint and using the admin token (sometimes referred to as the service token). The token is specified as the ``admin_token`` configuration option in your keystone.conf config file, which is typically in /etc/keystone:: + >>> from keystoneauth1.identity import v2 + >>> from keystoneauth1 import session >>> from keystoneclient.v2_0 import client >>> token = '012345SECRET99TOKEN012345' >>> endpoint = 'http://192.168.206.130:35357/v2.0' - >>> keystone = client.Client(token=token, endpoint=endpoint) + >>> auth = v2.Token(auth_url=endpoint, token=token) + >>> sess = session.Session(auth=auth) + >>> keystone = client.Client(session=sess) If you have a username and password, authentication is done against the public endpoint. You must also specify a tenant that is associated with the user:: + >>> from keystoneauth1.identity import v2 + >>> from keystoneauth1 import session >>> from keystoneclient.v2_0 import client >>> username='adminUser' - >>> password='secreetword' + >>> password='secretword' >>> tenant_name='openstackDemo' >>> auth_url='http://192.168.206.130:5000/v2.0' - >>> keystone = client.Client(username=username, password=password, - ... tenant_name=tenant_name, auth_url=auth_url) + >>> auth = v2.Password(username=username, password=password, + ... tenant_name=tenant_name, auth_url=auth_url) + >>> sess = session.Session(auth=auth) + >>> keystone = client.Client(session=sess) Creating tenants ================ -This example will create a tenant named *openStackDemo*:: +This example will create a tenant named *openstackDemo*:: >>> from keystoneclient.v2_0 import client >>> keystone = client.Client(...) >>> keystone.tenants.create(tenant_name="openstackDemo", ... description="Default Tenant", enabled=True) - + Creating users ============== This example will create a user named *adminUser* with a password *secretword* -in the opoenstackDemo tenant. We first need to retrieve the tenant:: +in the openstackDemo tenant. We first need to retrieve the tenant:: >>> from keystoneclient.v2_0 import client >>> keystone = client.Client(...) diff --git a/doc/source/using-api-v3.rst b/doc/source/using-api-v3.rst new file mode 100644 index 000000000..4f305e81f --- /dev/null +++ b/doc/source/using-api-v3.rst @@ -0,0 +1,165 @@ +======================= +Using the V3 Client API +======================= + +Introduction +============ + +The main concepts in the Identity v3 API are: + + * :py:mod:`~keystoneclient.v3.credentials` + * :py:mod:`~keystoneclient.v3.domain_configs` + * :py:mod:`~keystoneclient.v3.domains` + * :py:mod:`~keystoneclient.v3.endpoints` + * :py:mod:`~keystoneclient.v3.groups` + * :py:mod:`~keystoneclient.v3.policies` + * :py:mod:`~keystoneclient.v3.projects` + * :py:mod:`~keystoneclient.v3.regions` + * :py:mod:`~keystoneclient.v3.role_assignments` + * :py:mod:`~keystoneclient.v3.roles` + * :py:mod:`~keystoneclient.v3.services` + * :py:mod:`~keystoneclient.v3.tokens` + * :py:mod:`~keystoneclient.v3.users` + +The :py:mod:`keystoneclient.v3.client` API lets you query and make changes +through ``managers``. For example, to manipulate a project (formerly +called tenant), you interact with a +:py:class:`keystoneclient.v3.projects.ProjectManager` object. + +You obtain access to managers through attributes of a +:py:class:`keystoneclient.v3.client.Client` object. For example, the +``projects`` attribute of a ``Client`` object is a projects manager:: + + >>> from keystoneclient.v3 import client + >>> keystone = client.Client(...) + >>> keystone.projects.list() # List projects + +While it is possible to instantiate a +:py:class:`keystoneclient.v3.client.Client` object (as done above for +clarity), the recommended approach is to use the discovery mechanism +provided by the :py:class:`keystoneclient.client.Client` class. The +appropriate class will be instantiated depending on the API versions +available:: + + >>> from keystoneclient import client + >>> keystone = + ... client.Client(auth_url='http://localhost:5000', ...) + >>> type(keystone) + + +One can force the use of a specific version of the API, either by +using the ``version`` keyword argument:: + + >>> from keystoneclient import client + >>> keystone = client.Client(auth_url='http://localhost:5000', + version=(2,), ...) + >>> type(keystone) + + >>> keystone = client.Client(auth_url='http://localhost:5000', + version=(3,), ...) + >>> type(keystone) + + +Or by specifying directly the specific API version authentication URL +as the auth_url keyword argument:: + + >>> from keystoneclient import client + >>> keystone = + ... client.Client(auth_url='http://localhost:5000/v2.0', ...) + >>> type(keystone) + + >>> keystone = + ... client.Client(auth_url='http://localhost:5000/v3', ...) + >>> type(keystone) + + +Upon successful authentication, a :py:class:`keystoneclient.v3.client.Client` +object is returned (when using the Identity v3 API). Authentication and +examples of common tasks are provided below. + +You can generally expect that when the client needs to propagate an +exception it will raise an instance of subclass of +:class:`keystoneclient.exceptions.ClientException`. + +Authenticating Using Sessions +============================= + +Instantiate a :py:class:`keystoneclient.v3.client.Client` using a +:py:class:`~keystoneauth1.session.Session` to provide the authentication +plugin, SSL/TLS certificates, and other data:: + + >>> 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', + ... user_id='myuserid', + ... password='mypassword', + ... project_id='myprojectid') + >>> sess = session.Session(auth=auth) + >>> keystone = client.Client(session=sess) + +For more information on Sessions refer to: `Using Sessions`_. + +.. _`Using Sessions`: using-sessions.html + +Getting Metadata Responses +========================== + +Instantiating :py:class:`keystoneclient.v3.client.Client` using +`include_metadata=True` will cause manager response to return +:py:class:`keystoneclient.base.Response` instead of just the data. +The metadata property will be available directly to the +:py:class:`keystoneclient.base.Response` and the response data will +be available as property `data` to it. + + >>> 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', + ... user_id='myuserid', + ... password='mypassword', + ... project_id='myprojectid') + >>> sess = session.Session(auth=auth) + >>> keystone = client.Client(session=sess, include_metadata=True) + >>> resp = keystone.projects.list() + >>> resp.request_ids[0] + req-1234-5678-... + >>> resp.data + [, , ...] + +Non-Session Authentication (deprecated) +======================================= + +The *deprecated* way to authenticate is to pass the username, the user's domain +name (which will default to 'Default' if it is not specified), and a +password:: + + >>> from keystoneclient import client + >>> auth_url = 'http://localhost:5000' + >>> username = 'adminUser' + >>> user_domain_name = 'Default' + >>> password = 'secreetword' + >>> keystone = client.Client(auth_url=auth_url, version=(3,), + ... username=username, password=password, + ... user_domain_name=user_domain_name) + +A :py:class:`~keystoneauth1.session.Session` should be passed to the Client +instead. Using a Session you're not limited to authentication using a username +and password but can take advantage of other more secure authentication +methods. + +You may optionally specify a domain or project (along with its project +domain name), to obtain a scoped token:: + + >>> from keystoneclient import client + >>> auth_url = 'http://localhost:5000' + >>> username = 'adminUser' + >>> user_domain_name = 'Default' + >>> project_name = 'demo' + >>> project_domain_name = 'Default' + >>> password = 'secreetword' + >>> keystone = client.Client(auth_url=auth_url, version=(3,), + ... username=username, password=password, + ... user_domain_name=user_domain_name, + ... project_name=project_name, + ... project_domain_name=project_domain_name) diff --git a/doc/source/using-sessions.rst b/doc/source/using-sessions.rst new file mode 100644 index 000000000..a5a5a58da --- /dev/null +++ b/doc/source/using-sessions.rst @@ -0,0 +1,200 @@ +============== +Using Sessions +============== + +Introduction +============ + +The :py:class:`keystoneauth1.session.Session` class was introduced into +keystoneclient as an attempt to bring a unified interface to the various +OpenStack clients that share common authentication and request parameters +between a variety of services. + +The model for using a Session and auth plugin as well as the general terms used +have been heavily inspired by the `requests `_ +library. However neither the Session class nor any of the authentication +plugins rely directly on those concepts from the requests library so you should +not expect a direct translation. + +Features +-------- + +- Common client authentication + + Authentication is handled by one of a variety of authentication plugins and + then this authentication information is shared between all the services that + use the same Session object. + +- Security maintenance + + Security code is maintained in a single place and reused between all + clients such that in the event of problems it can be fixed in a single + location. + +- Standard discovery mechanisms + + Clients are not expected to have any knowledge of an identity token or any + other form of identification credential. Service and endpoint discovery are + handled by the Session and plugins. + + +Sessions for Users +================== + +The Session object is the contact point to your OpenStack cloud services. It +stores the authentication credentials and connection information required to +communicate with OpenStack such that it can be reused to communicate with many +services. When creating services this Session object is passed to the client +so that it may use this information. + +A Session will authenticate on demand. When a request that requires +authentication passes through the Session the authentication plugin will be +asked for a valid token. If a valid token is available it will be used +otherwise the authentication plugin may attempt to contact the authentication +service and fetch a new one. + +An example from keystoneclient:: + + >>> from keystoneauth1.identity import v3 + >>> from keystoneauth1 import session + >>> from keystoneclient.v3 import client + + >>> auth = v3.Password(auth_url='https://my.keystone.com:5000/v3', + ... username='myuser', + ... password='mypassword', + ... project_id='proj', + ... user_domain_id='domain') + >>> sess = session.Session(auth=auth, + ... verify='/path/to/ca.cert') + >>> ks = client.Client(session=sess) + >>> users = ks.users.list() + +As clients adopt this means of operating they will be created in a similar +fashion by passing the Session object to the client's constructor. + + +Migrating keystoneclient to use a Session +----------------------------------------- + +By using a session with a keystoneclient Client we presume that you have opted +in to new behavior defined by the session. For example authentication is now +on-demand rather than on creation. To allow this change in behavior there are +a number of functions that have changed behavior or are no longer available. + +For example the +:py:meth:`keystoneclient.httpclient.HTTPClient.authenticate` method used +to be able to always re-authenticate the current client and fetch a new token. +As this is now controlled by the Session and not the client this has changed, +however the function will still exist to provide compatibility with older +clients. + +Likewise certain parameters such as ``user_id`` and ``auth_token`` that used to +be available on the client object post authentication will remain +uninitialized. + +When converting an application to use a session object with keystoneclient you +should be aware of the possibility of changes to authentication and +authentication parameters and make sure to test your code thoroughly. It should +have no impact on the typical CRUD interaction with the client. + + +Sharing Authentication Plugins +------------------------------ + +A session can only contain one authentication plugin however there is nothing +that specifically binds the authentication plugin to that session, a new +Session can be created that reuses the existing authentication plugin:: + + >>> new_sess = session.Session(auth=sess.auth, + verify='/path/to/different-cas.cert') + +In this case we cannot know which session object will be used when the plugin +performs the authentication call so the command must be able to succeed with +either. + +Authentication plugins can also be provided on a per-request basis. This will +be beneficial in a situation where a single session is juggling multiple +authentication credentials:: + + >>> sess.get('https://my.keystone.com:5000/v3', + auth=my_auth_plugin) + +If an auth plugin is provided via parameter then it will override any auth +plugin on the session. + +Sessions for Client Developers +============================== + +Sessions are intended to take away much of the hassle of dealing with +authentication data and token formats. Clients should be able to specify filter +parameters for selecting the endpoint and have the parsing of the catalog +managed for them. + +Authentication +-------------- + +When making a request with a session object you can simply pass the keyword +parameter ``authenticated`` to indicate whether the argument should contain a +token, by default a token is included if an authentication plugin is available:: + + >>> # In keystone this route is unprotected by default + >>> resp = sess.get('https://my.keystone.com:5000/v3', + authenticated=False) + + +Service Discovery +----------------- + +In OpenStack the URLs of available services are distributed to the user as a +part of the token they receive called the Service Catalog. Clients are expected +to use the URLs from the Service Catalog rather than have them provided. + +In general a client does not need to know the full URL for the server that they +are communicating with, simply that it should send a request to a path +belonging to the correct service. + +This is controlled by the ``endpoint_filter`` parameter to a request which +contains all the information an authentication plugin requires to determine the +correct URL to which to send a request. When using this mode only the path for +the request needs to be specified:: + + >>> resp = session.get('/v3/users', + endpoint_filter={'service_type': 'identity', + 'interface': 'public', + 'region_name': 'myregion'}) + +``endpoint_filter`` accepts a number of arguments with which it can determine +an endpoint url: + +- ``service_type``: the type of service. For example ``identity``, ``compute``, + ``volume`` or many other predefined identifiers. + +- ``interface``: the network exposure the interface has. This will be one of: + + - ``public``: An endpoint that is available to the wider internet or network. + - ``internal``: An endpoint that is only accessible within the private network. + - ``admin``: An endpoint to be used for administrative tasks. + +- ``region_name``: the name of the region where the endpoint resides. + +The endpoint filter is a simple key-value filter and can be provided with any +number of arguments. It is then up to the auth plugin to correctly use the +parameters it understands. + +The session object determines the URL matching the filter and append to it the +provided path and so create a valid request. If multiple URL matches are found +then any one may be chosen. + +While authentication plugins will endeavour to maintain a consistent set of +arguments for an ``endpoint_filter`` the concept of an authentication plugin is +purposefully generic and a specific mechanism may not know how to interpret +certain arguments and ignore them. For example the +:py:class:`keystoneauth1.identity.generic.token.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. diff --git a/examples/pki/certs/cacert.pem b/examples/pki/certs/cacert.pem index 8cf663cf9..6519671a5 100644 --- a/examples/pki/certs/cacert.pem +++ b/examples/pki/certs/cacert.pem @@ -1,18 +1,23 @@ -----BEGIN CERTIFICATE----- -MIIC0TCCAjqgAwIBAgIJAP2TNFqmE1KUMA0GCSqGSIb3DQEBBQUAMIGeMQowCAYD -VQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55 -dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMG -CSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2Vs -ZiBTaWduZWQwIBcNMTIxMTExMTA1NDA2WhgPMjA3MTA1MDYxMDU0MDZaMIGeMQow -CAYDVQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1 -bm55dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTEl -MCMGCSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxML -U2VsZiBTaWduZWQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMXgnd5wlHAp -GxZ58LrpEkHU995lT9PxtMgkp0tpFhg7R5HQw9K7TfQk5NHB28hNzf8UE/c0z2pJ -XggPnAzvdx27NQeJGX5CWsi6fITZ8vH/+SxgfxxC+CE/6BkDpzw21MgBtq11vWL7 -XVaxNeU12Ax889U66i3CrObuCYt2mbpzAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMB -Af8wDQYJKoZIhvcNAQEFBQADgYEAkFIbnr2/0/XWp+f80Gl6GAC7tdmZFlT9udVF -q794rXyMlYY64pq34SzfQAn+4DztT4B9yzrTx03tLNr6Uf+5TS+ubcwG41UBBMs/ -Icf9zBMRqr+IXhijS49gQ7dPjqNTCqX+6ILbRWjdXP15ZWymI3ayQL/CMwFt/E+0 -kT6MLes= +MIID4TCCAsmgAwIBAgIUD+MH2KCtZgLgFWNghxF+xZQZx98wDQYJKoZIhvcNAQEL +BQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAG +A1UEBwwJU3Vubnl2YWxlMRIwEAYDVQQKDAlPcGVuU3RhY2sxETAPBgNVBAsMCEtl +eXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQw +EgYDVQQDDAtTZWxmIFNpZ25lZDAgFw0yMjAzMDcxNTM1MThaGA8yMDgwMDgyOTE1 +MzUxOFowgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTES +MBAGA1UEBwwJU3Vubnl2YWxlMRIwEAYDVQQKDAlPcGVuU3RhY2sxETAPBgNVBAsM +CEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3Jn +MRQwEgYDVQQDDAtTZWxmIFNpZ25lZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAL/+AsUcqyF2b/3gaHGtUZx+mReX2QMv+gXmW2KUj1CTmSTxGXCaeoJ5 +N7PA6BeBD/HJVqTgCo/oNuHmOgtYrgRngyWpABItt9ONRmTCr2AvA23AZIjfUdwR +ZceRHf67H6N1NOttr8IFkuQFhTAKuRHJGcXNqNMJrNv2v5ha3GNeAhxZd965ok9B +GSd+hvibjZ2mDBZ8kiJ9BGf53TDie/zg+q5CkgqLArgR30pGCe+ZLXPLrhekpyet +BR3guKTV2PMCeIh7Yg/uTJMe22qZ87M6Q1DosSKC/1l+/1ArBve6msc8JEElnc32 +HJ7NuTTJreZKEvPmUI2oTcdvokWXtRsCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEAmdWRPzpI5pAoVn9GX1KNgiN/e9oCpnHYIofV +fX7i70OBYOwBoyQhFqniHtGH4uqYIxGJdbDtHzsSYCSV1mGqHhK+kStLy4MULUUV +cNM5yDYDPEtjgJy7G90z/ksX5WuXQgktx9N8emdI6yH8C1b6sHMtHcfnb6O0waMH +HQ9QpZFQapwuIjeWU0zRDFZkdEAkx6wfVuoMhHOjy1WRAuIOL2ELa6h0GL2d+bmw +x4Xpyi4X7pgixz3l/9Kfc6VdVrEy4H2bhldUeZ0WjzvMdYaw953+C/5YAfFYanCH +en9BebSKQMv8QI0OrNyTefMXuxWvcKSOWjQVfRk1ckz6aIrfSw== -----END CERTIFICATE----- diff --git a/examples/pki/certs/middleware.pem b/examples/pki/certs/middleware.pem deleted file mode 100644 index ae5b8dbb2..000000000 --- a/examples/pki/certs/middleware.pem +++ /dev/null @@ -1,33 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICoTCCAgoCARAwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK -EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr -ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x -MjExMTExMDU0MDZaGA8yMDcxMDUwNjEwNTQwNlowgZAxCzAJBgNVBAYTAlVTMQsw -CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh -Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv -cGVuc3RhY2sub3JnMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEB -BQADgY0AMIGJAoGBALVu4bjaOH33yAx0WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+ -cUriQtxf6ahjxtzLPERS81SnwZmrICWZngbOn733pULMTZktTJH+o7C74NdKwUSN -xjlCeWUy+FqIQoje4ygoJRPpMdkp1wHNO0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMB -AAEwDQYJKoZIhvcNAQEFBQADgYEAcp9ancue9Oq+MkaPucCrIqFhiUsdUThulJlB -etPpUDGgStBSHgze/oxG2+flIjRoI6gG9Chfw//vWHOwDT7N32AHSgaI4b8/k/+s -hAV2khYkV4PW2oS1TfeU/vxQzXbgApqhLBNqfFmJVW48aGAr/aqsJi3MYWN3269+ -6vChaVw= ------END CERTIFICATE----- ------BEGIN PRIVATE KEY----- -MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALVu4bjaOH33yAx0 -WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+cUriQtxf6ahjxtzLPERS81SnwZmrICWZ -ngbOn733pULMTZktTJH+o7C74NdKwUSNxjlCeWUy+FqIQoje4ygoJRPpMdkp1wHN -O0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMBAAECgYAmwq6EYFJrTvE0//JmN/8qzfvg -dI5PoWpD+F8UInUxr2T2tHOdrOLd07vGVrKYXu7cJeCIOGKa4r02azAggioL/nE9 -FgPpqEC+QROvLuhFsk1gLZ2pGQ06sveKZVMH22h59BKZkYlhjh5qd4vlmhPqkmPp -gdXj7ZjDCJhhQdFVkQJBANp18k2mVksn8q29LMieVTSIZNN3ucDA1QHbim+3fp/O -GxCzU7Mv1Xfnu1zoRFu5/sF3YG0Zy3TGPDrEljBC3rUCQQDUnBjVFXL35OkBZqXW -taJPzGbsPoqAO+Ls2juS97zNzeGxUNhvcKuEvHO63PXqDxp1535DpvJEBN1rT2FF -iaO1AkEAt/QTWWFUTqrPxY6DNFdm5fpn9E1fg7icZJkKBDJeFJCH59MpCryfovzl -n0ERtq9ynlQ4RQYwdR8rvkylLvRP9QJAOiXHFOAc5XeR0nREfwiGL9TzgUFJl/DJ -C4ZULMnctVzNkTVPPItQHal87WppR26CCiUZ/161e6zo8eRv8hjG0QJABWqfYQuK -dWH8nxlXS+NFUDbsCdL+XpOVE7iEH7hvSw/A/kz40mLx8sDp/Fz1ysrogR/L+NGC -Vrlwm4q/WYJO0Q== ------END PRIVATE KEY----- diff --git a/examples/pki/certs/signing_cert.pem b/examples/pki/certs/signing_cert.pem index 1491b55d7..6428be8d0 100644 --- a/examples/pki/certs/signing_cert.pem +++ b/examples/pki/certs/signing_cert.pem @@ -1,17 +1,22 @@ -----BEGIN CERTIFICATE----- -MIICoDCCAgkCAREwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK -EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr -ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x -MjExMTExMDU0MDZaGA8yMDcxMDUwNjEwNTQwNlowgY8xCzAJBgNVBAYTAlVTMQsw -CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh -Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv -cGVuc3RhY2sub3JnMREwDwYDVQQDEwhLZXlzdG9uZTCBnzANBgkqhkiG9w0BAQEF -AAOBjQAwgYkCgYEAuoQC6IBqMxC5845c/ZkLsdcQbTHqIpYJHEkwEoxyeEjwiGFf -iZmiZ91pSFNc9MfjdJnN+be/ndVS19w1nrrJvV/udVsf6JZWkTPX5HyxnllwznCH -pP7gfvMZzGsqzWlSdiD6mcRbCYRX9hCCauG3jhCtISINCVYMYQGH6QSib9sCAwEA -ATANBgkqhkiG9w0BAQUFAAOBgQBCssELi+1RSjEmzeqSnpgUqmtpvB9oxbcwl+xH -rIrYvqMU6pV2aSxgLDqpGjjusLHUau9Bmu3Myc/fm9/mlPUQHNj0AWl8vvfSlq1b -vsWMUa1h4UFlPWoF2DIUFd+noBxe5CbcLUV6K0oyJAcPO433OyuGl5oQkhxmoy1J -w59KRg== +MIIDpTCCAo0CAREwDQYJKoZIhvcNAQELBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU3Vubnl2YWxlMRIwEAYDVQQK +DAlPcGVuU3RhY2sxETAPBgNVBAsMCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr +ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDDAtTZWxmIFNpZ25lZDAgFw0y +MjAzMDcxNTM1MThaGA8yMDgwMDgyOTE1MzUxOFowgY8xCzAJBgNVBAYTAlVTMQsw +CQYDVQQIDAJDQTESMBAGA1UEBwwJU3Vubnl2YWxlMRIwEAYDVQQKDAlPcGVuU3Rh +Y2sxETAPBgNVBAsMCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv +cGVuc3RhY2sub3JnMREwDwYDVQQDDAhLZXlzdG9uZTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAJ7Gc/CGy82PWOmlGD+C8d6Kw3Q1ZodOz9EduhXecfLN ++zHXd9Qd35f5hHf4c8j0yAxI8qfeabjnOefEkoHC4W+DTTsUwq1x7FvAPHbTncSH +zePqBE8wKp4pfFuZAhMxP55nTJC/N8t1M1lUqYTANgTCAystyOJuLuMc03UBqO8b +OGzCJsAdcsBPpdYkfycrWv5ZosdaHf3OmakPtWymjSPQ8/lM4x5Fm5GaoYUqb9mo +busMp0te7MMkzWYilSqZBWHx7dsGR7HoN4zMqalttC0inJGc0wnusNrkeb3ieuXw +U6T3V3pE3yTTuHy6HcZZd/8m3O/L9F1odzDnUH10PjkCAwEAATANBgkqhkiG9w0B +AQsFAAOCAQEAiSaPtpERflBUDYtRrAPVEyM3K9DJyZ8pv1vQxCU/h4ZNttVWsRdC +gqdZg8nYSLj81ZwU1OATQhjXjGn9/mYAIzbm+HH1TMJDWqmnkSblAHGPZmswKmga +/Cns8PsgsLcMV9BA38lyBhVtgBn4QgsG9EUvscZvVUnxevgqg3a/tlfpPf7fvbmC +Efcq3liI/l+wxv4O3ET3V6rBZsmTUMNrIIhqFcicynUy3NIIRO3mL92se9X81Jpj +YxHtMt+RakM0P7yRYL2hjgQW2srssGlMt9U/OIEZQKJVBH85qYuoBAcXC7Y6xRy1 +LvQc4IKf3X4hmqZC7jhBIQAAbaDZTn8peg== -----END CERTIFICATE----- diff --git a/examples/pki/certs/ssl_cert.pem b/examples/pki/certs/ssl_cert.pem index 0a0bc215e..00d33a7bb 100644 --- a/examples/pki/certs/ssl_cert.pem +++ b/examples/pki/certs/ssl_cert.pem @@ -1,17 +1,22 @@ -----BEGIN CERTIFICATE----- -MIICoTCCAgoCARAwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK -EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr -ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x -MjExMTExMDU0MDZaGA8yMDcxMDUwNjEwNTQwNlowgZAxCzAJBgNVBAYTAlVTMQsw -CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh -Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv -cGVuc3RhY2sub3JnMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEB -BQADgY0AMIGJAoGBALVu4bjaOH33yAx0WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+ -cUriQtxf6ahjxtzLPERS81SnwZmrICWZngbOn733pULMTZktTJH+o7C74NdKwUSN -xjlCeWUy+FqIQoje4ygoJRPpMdkp1wHNO0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMB -AAEwDQYJKoZIhvcNAQEFBQADgYEAcp9ancue9Oq+MkaPucCrIqFhiUsdUThulJlB -etPpUDGgStBSHgze/oxG2+flIjRoI6gG9Chfw//vWHOwDT7N32AHSgaI4b8/k/+s -hAV2khYkV4PW2oS1TfeU/vxQzXbgApqhLBNqfFmJVW48aGAr/aqsJi3MYWN3269+ -6vChaVw= +MIIDpjCCAo4CARAwDQYJKoZIhvcNAQELBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU3Vubnl2YWxlMRIwEAYDVQQK +DAlPcGVuU3RhY2sxETAPBgNVBAsMCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr +ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDDAtTZWxmIFNpZ25lZDAgFw0y +MjAzMDcxNTM1MThaGA8yMDgwMDgyOTE1MzUxOFowgZAxCzAJBgNVBAYTAlVTMQsw +CQYDVQQIDAJDQTESMBAGA1UEBwwJU3Vubnl2YWxlMRIwEAYDVQQKDAlPcGVuU3Rh +Y2sxETAPBgNVBAsMCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv +cGVuc3RhY2sub3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQD1i9+ydZSypNAkkVXdzIqZ8E62cqH7i0JGVGBuGdH8 +ZVF3MDcbi8VwqfNRWoWn9mrJUp5HYDV9t5WXz25Ej4EnqlJ3WLZvC1e+ldDIInmi +ULic3iIAgrWbumU3XNLHska/smoVJuLIuFUxEfRpwGOpguOzAO1M6BKCSwr+TBLY +JZxc3F7v1vtwNhisyE5S2H6Q49K0UXHTPjp+fLZAHQ5+Yxqwf0KAJqAD3vMo8Ewx +XlgJu8pQyjjxwtrwnN2WHYoJGt/OOdkLBbzdupWH9CGcxeVc5hSJ1hEKuYS8AOZI +eH6q2MKwT+QiepBsVfuy1JFt4raLht/RcX/WR8lIAzoFAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAArxwP6I5XXIl3Dhkkt6gegRNc1vYWPEIDkKqggnvntZAOZwavVQ +kiydT09io82SjD3qv/PQFH+N1KkTCIgYreHQpCQaWMvkpCD2iEcu9R75p4rnZMR2 +NwIlj4BHvXIo9ET5dkhUUxzUGK7eIymNEoMWMF6OGlQhK1FV3Tvjum0sqLOyKOgr +NFxDv7qFzoKfqjY3lfb9yqO7xC1t3CZSOsBLIaUQ9SBoRJ11UNYGq9ZXHNF3cCbC +PyE1TgVjNEvWBBRY0ofGoPmdqrTe2oZ6rAFKf0aWJ1qIq+umePp9R8ZwWLonbxQ4 +0nqaUI5AOAdsRpUJHGvW0mmMALYjHT+tF8U= -----END CERTIFICATE----- diff --git a/examples/pki/cms/auth_token_revoked.json b/examples/pki/cms/auth_token_revoked.json index 92c6922c4..eab40a0bc 100644 --- a/examples/pki/cms/auth_token_revoked.json +++ b/examples/pki/cms/auth_token_revoked.json @@ -1 +1,88 @@ -{"access": {"serviceCatalog": [{"endpoints": [{"adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "volume", "name": "volume"}, {"endpoints": [{"adminURL": "http://127.0.0.1:9292/v1", "region": "regionOne", "internalURL": "http://127.0.0.1:9292/v1", "publicURL": "http://127.0.0.1:9292/v1"}], "endpoints_links": [], "type": "image", "name": "glance"}, {"endpoints": [{"adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "compute", "name": "nova"}, {"endpoints": [{"adminURL": "http://127.0.0.1:35357/v2.0", "region": "RegionOne", "internalURL": "http://127.0.0.1:35357/v2.0", "publicURL": "http://127.0.0.1:5000/v2.0"}], "endpoints_links": [], "type": "identity", "name": "keystone"}],"token": {"expires": "2012-06-02T14:47:34Z", "id": "placeholder", "tenant": {"enabled": true, "description": null, "name": "tenant_name1", "id": "tenant_id1"}}, "user": {"username": "revoked_username1", "roles_links": ["role1","role2"], "id": "revoked_user_id1", "roles": [{"name": "role1"}, {"name": "role2"}], "name": "revoked_username1"}}} +{ + "access": { + "token": { + "expires": "2038-01-18T21:14:07Z", + "issued_at": "2002-01-18T21:14:07Z", + "id": "placeholder", + "tenant": { + "id": "tenant_id1", + "enabled": true, + "description": null, + "name": "tenant_name1" + } + }, + "serviceCatalog": [ + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne", + "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a" + } + ], + "type": "volume", + "name": "volume" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:9292/v1", + "region": "regionOne", + "internalURL": "http://127.0.0.1:9292/v1", + "publicURL": "http://127.0.0.1:9292/v1" + } + ], + "type": "image", + "name": "glance" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne", + "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a" + } + ], + "type": "compute", + "name": "nova" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:35357/v2.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:35357/v2.0", + "publicURL": "http://127.0.0.1:5000/v2.0" + } + ], + "type": "identity", + "name": "keystone" + } + ], + "user": { + "username": "revoked_username1", + "roles_links": [ + "role1", + "role2" + ], + "id": "revoked_user_id1", + "roles": [ + { + "id": "f03fda8f8a3249b2a70fb1f176a7b631", + "name": "role1" + }, + { + "id": "f03fda8f8a3249b2a70fb1f176a7b631", + "name": "role2" + } + ], + "name": "revoked_username1" + } + } +} diff --git a/examples/pki/cms/auth_token_revoked.pem b/examples/pki/cms/auth_token_revoked.pem index 0bd7a7088..b0312a9b0 100644 --- a/examples/pki/cms/auth_token_revoked.pem +++ b/examples/pki/cms/auth_token_revoked.pem @@ -1,42 +1,79 @@ -----BEGIN CMS----- -MIIHVgYJKoZIhvcNAQcCoIIHRzCCB0MCAQExCTAHBgUrDgMCGjCCBeQGCSqGSIb3 -DQEHAaCCBdUEggXReyJhY2Nlc3MiOiB7InNlcnZpY2VDYXRhbG9nIjogW3siZW5k -cG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2L3Yx -LzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInJlZ2lvbiI6ICJy -ZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2 -L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInB1YmxpY1VS -TCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThh -NjBmY2Y4OWJiNjYxN2EifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUi -OiAidm9sdW1lIiwgIm5hbWUiOiAidm9sdW1lIn0sIHsiZW5kcG9pbnRzIjogW3si -YWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwgInJlZ2lvbiI6 -ICJyZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5 -MjkyL3YxIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjkyOTIvdjEi -fV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAiaW1hZ2UiLCAibmFt -ZSI6ICJnbGFuY2UifSwgeyJlbmRwb2ludHMiOiBbeyJhZG1pblVSTCI6ICJodHRw -Oi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5 -YmI2NjE3YSIsICJyZWdpb24iOiAicmVnaW9uT25lIiwgImludGVybmFsVVJMIjog -Imh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYw -ZmNmODliYjY2MTdhIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3 -NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSJ9XSwgImVu -ZHBvaW50c19saW5rcyI6IFtdLCAidHlwZSI6ICJjb21wdXRlIiwgIm5hbWUiOiAi -bm92YSJ9LCB7ImVuZHBvaW50cyI6IFt7ImFkbWluVVJMIjogImh0dHA6Ly8xMjcu -MC4wLjE6MzUzNTcvdjIuMCIsICJyZWdpb24iOiAiUmVnaW9uT25lIiwgImludGVy -bmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjIuMCIsICJwdWJsaWNV -UkwiOiAiaHR0cDovLzEyNy4wLjAuMTo1MDAwL3YyLjAifV0sICJlbmRwb2ludHNf -bGlua3MiOiBbXSwgInR5cGUiOiAiaWRlbnRpdHkiLCAibmFtZSI6ICJrZXlzdG9u -ZSJ9XSwidG9rZW4iOiB7ImV4cGlyZXMiOiAiMjAxMi0wNi0wMlQxNDo0NzozNFoi -LCAiaWQiOiAicGxhY2Vob2xkZXIiLCAidGVuYW50IjogeyJlbmFibGVkIjogdHJ1 -ZSwgImRlc2NyaXB0aW9uIjogbnVsbCwgIm5hbWUiOiAidGVuYW50X25hbWUxIiwg -ImlkIjogInRlbmFudF9pZDEifX0sICJ1c2VyIjogeyJ1c2VybmFtZSI6ICJyZXZv -a2VkX3VzZXJuYW1lMSIsICJyb2xlc19saW5rcyI6IFsicm9sZTEiLCJyb2xlMiJd -LCAiaWQiOiAicmV2b2tlZF91c2VyX2lkMSIsICJyb2xlcyI6IFt7Im5hbWUiOiAi -cm9sZTEifSwgeyJuYW1lIjogInJvbGUyIn1dLCAibmFtZSI6ICJyZXZva2VkX3Vz -ZXJuYW1lMSJ9fX0NCjGCAUkwggFFAgEBMIGkMIGeMQowCAYDVQQFEwE1MQswCQYD -VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55dmFsZTESMBAGA1UE -ChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMGCSqGSIb3DQEJARYW -a2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2VsZiBTaWduZWQCAREw -BwYFKw4DAhowDQYJKoZIhvcNAQEBBQAEgYBhV5KrVjcdACPUNafkPY+lgCSlh6uc -N55SATBcQmg1/argEUFg/cx2GcF7ftQV384iGepLEgsq+6om2wPw6DWA0RknpVLJ -vMsHbWdGoXIZ5jRuAQTPtkXcJQOR677baDHvGJ+5zwBBDT2CmN2Tcv348+Xpjp7D -hF/cmAXnYYo00g== +MIIOVQYJKoZIhvcNAQcCoIIORjCCDkICAQExDTALBglghkgBZQMEAgEwggxaBgkq +hkiG9w0BBwGgggxLBIIMR3sNCiAgICAiYWNjZXNzIjogew0KICAgICAgICAidG9r +ZW4iOiB7DQogICAgICAgICAgICAiZXhwaXJlcyI6ICIyMDM4LTAxLTE4VDIxOjE0 +OjA3WiIsDQogICAgICAgICAgICAiaXNzdWVkX2F0IjogIjIwMDItMDEtMThUMjE6 +MTQ6MDdaIiwNCiAgICAgICAgICAgICJpZCI6ICJwbGFjZWhvbGRlciIsDQogICAg +ICAgICAgICAidGVuYW50Ijogew0KICAgICAgICAgICAgICAgICJpZCI6ICJ0ZW5h +bnRfaWQxIiwNCiAgICAgICAgICAgICAgICAiZW5hYmxlZCI6IHRydWUsDQogICAg +ICAgICAgICAgICAgImRlc2NyaXB0aW9uIjogbnVsbCwNCiAgICAgICAgICAgICAg +ICAibmFtZSI6ICJ0ZW5hbnRfbmFtZTEiDQogICAgICAgICAgICB9DQogICAgICAg +IH0sDQogICAgICAgICJzZXJ2aWNlQ2F0YWxvZyI6IFsNCiAgICAgICAgICAgIHsN +CiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAgICAg +ICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAgew0K +ICAgICAgICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6Ly8xMjcu +MC4wLjE6ODc3Ni92MS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIs +DQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSIs +DQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDov +LzEyNy4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2 +MTdhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0 +cDovLzEyNy4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODli +YjY2MTdhIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAg +XSwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJ2b2x1bWUiLA0KICAgICAgICAg +ICAgICAgICJuYW1lIjogInZvbHVtZSINCiAgICAgICAgICAgIH0sDQogICAgICAg +ICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0K +ICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAg +ICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRw +Oi8vMTI3LjAuMC4xOjkyOTIvdjEiLA0KICAgICAgICAgICAgICAgICAgICAgICAg +InJlZ2lvbiI6ICJyZWdpb25PbmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAg +ImludGVybmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQogICAg +ICAgICAgICAgICAgICAgICAgICAicHVibGljVVJMIjogImh0dHA6Ly8xMjcuMC4w +LjE6OTI5Mi92MSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAg +ICAgIF0sDQogICAgICAgICAgICAgICAgInR5cGUiOiAiaW1hZ2UiLA0KICAgICAg +ICAgICAgICAgICJuYW1lIjogImdsYW5jZSINCiAgICAgICAgICAgIH0sDQogICAg +ICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtd +LA0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAg +ICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJo +dHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZj +Zjg5YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjog +InJlZ2lvbk9uZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxV +UkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YxLjEvNjRiNmYzZmJjYzUzNDM1 +ZThhNjBmY2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInB1 +YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNj +NTM0MzVlOGE2MGZjZjg5YmI2NjE3YSINCiAgICAgICAgICAgICAgICAgICAgfQ0K +ICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgInR5cGUiOiAiY29t +cHV0ZSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAibm92YSINCiAgICAgICAg +ICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50 +c19saW5rcyI6IFtdLA0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQog +ICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJh +ZG1pblVSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjM1MzU3L3YyLjAiLA0KICAgICAg +ICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6ICJSZWdpb25PbmUiLA0KICAgICAg +ICAgICAgICAgICAgICAgICAgImludGVybmFsVVJMIjogImh0dHA6Ly8xMjcuMC4w +LjE6MzUzNTcvdjIuMCIsDQogICAgICAgICAgICAgICAgICAgICAgICAicHVibGlj +VVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6NTAwMC92Mi4wIg0KICAgICAgICAgICAg +ICAgICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAi +dHlwZSI6ICJpZGVudGl0eSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAia2V5 +c3RvbmUiDQogICAgICAgICAgICB9DQogICAgICAgIF0sDQogICAgICAgICJ1c2Vy +Ijogew0KICAgICAgICAgICAgInVzZXJuYW1lIjogInJldm9rZWRfdXNlcm5hbWUx +IiwNCiAgICAgICAgICAgICJyb2xlc19saW5rcyI6IFsNCiAgICAgICAgICAgICAg +ICAicm9sZTEiLA0KICAgICAgICAgICAgICAgICJyb2xlMiINCiAgICAgICAgICAg +IF0sDQogICAgICAgICAgICAiaWQiOiAicmV2b2tlZF91c2VyX2lkMSIsDQogICAg +ICAgICAgICAicm9sZXMiOiBbDQogICAgICAgICAgICAgICAgew0KICAgICAgICAg +ICAgICAgICAgICAiaWQiOiAiZjAzZmRhOGY4YTMyNDliMmE3MGZiMWYxNzZhN2I2 +MzEiLA0KICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJyb2xlMSINCiAgICAg +ICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAg +ICAgICAgImlkIjogImYwM2ZkYThmOGEzMjQ5YjJhNzBmYjFmMTc2YTdiNjMxIiwN +CiAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAicm9sZTIiDQogICAgICAgICAg +ICAgICAgfQ0KICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICJuYW1lIjogInJl +dm9rZWRfdXNlcm5hbWUxIg0KICAgICAgICB9DQogICAgfQ0KfQ0KMYIBzjCCAcoC +AQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTES +MBAGA1UEBwwJU3Vubnl2YWxlMRIwEAYDVQQKDAlPcGVuU3RhY2sxETAPBgNVBAsM +CEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3Jn +MRQwEgYDVQQDDAtTZWxmIFNpZ25lZAIBETALBglghkgBZQMEAgEwDQYJKoZIhvcN +AQEBBQAEggEAGUm9+Jb4P4dO23cAg39q0vVDFPkiPxgxakKE+g4d7VSI7Krt5ypB +iXo4mHbLqM28zrxgCBxnq2ZhhGzk/qhVWadYWUQQ9FjUBna06Cbd5clGpP8Kp2yk ++SRydFZAw9jUviditNhZA7Nhl8Qzu6T9TB+jPI2y1PY/XXebN97dQqmc+QMGVfzz +ssU+89ASfki8vEDJWxn2RtxjaXPKeWFUw/qrvj1RkDdI3d22dOTR9p0q7Y9CKLam +Y1DkosGbBqUF8hTtJMXIfHTLh5Zp+zz1dlsY0FR9kxLKxKya9OG3ACyidL7ewM9r +pEz2NQztAoi3Guxj6793G0Sfgb0ZCTGcaA== -----END CMS----- diff --git a/examples/pki/cms/auth_token_revoked.pkiz b/examples/pki/cms/auth_token_revoked.pkiz new file mode 100644 index 000000000..aff7b74ff --- /dev/null +++ b/examples/pki/cms/auth_token_revoked.pkiz @@ -0,0 +1 @@ +PKIZ_eJylVkt3ozgT3etXzD6nTwBjJ17MQjyMRSzZYAxIOwMJIB524gePX_8Jk0xn0n2me77xxlCC4ureW1X69k38NNNC5A8db4ebbwAjhHaQ2k8HhrJrTKAT6wcRczxd1w1Jh47Z6h5caunuzUixbnFd10rJ0rev1hZFE2A45hLuRbBQzTRlT8-dnVGFlPEE5-tce0C1e90r_gXxQyrWjvGEyCxwX2joiHWYA8xhg3OpwVupXS-cDnuHlhiHhsiHfKXDnIVZsw_tMu7QDOmowwZWVx5sV56p-gZqwZqb0prDSZCjE9LtI9OHB-0mshacBdk1stwyHtckFkyzqHZGZJV_oYF9Aiy4BaS49svhi_tghJZYwwNTKVTKAm9vCcS9XA5bEdsqo2pxSRbzCxiC7w8ULCQ8rsomscprlAsk1lSOrHb-sm3ES4KXmh2p4hs0dLPImtdDMhBMTrmAVsTW_BjVbj8EhxgN3PM-mPq7orkh2i9dKTYO11VvdqRTmxWHF8GXCkgfK6sJbVc9lShnFVZYThUs497pSbBTqUcbVpFqbZQ55WLFSzKUD4jskinlFdyg6nbHguQYKdNNVO3ykYupxMJh3-0_ogADjP8fhSYDWrVHKvtbb5TvkCzdZp0_XrGHJrd96mq75umE9PSacPNKuJuTivassjntdz0gBpaZl2WEaxVVqLoO7Jxw2hLFzF98aVBHSOY2kVJekiV5iazysh9dGoVCHSA0ncbWbtS-mp-SQesBXiVME3yJ1yLh8u-qgX8p2xTzohv4-lACDFL8FyXAzzNr8u-RW3Rg7aGB3d8i7DNf-0DOmLLLwQBVFMaZbW9fqkUVXqhYGPwv6v9TwjHR0C-YJR-jckQH_ll7Z0B3wdtHhVhIYVw4oCKceFjCvV-uLVMB2GKc8XRKK6QQbk7oWJnvhKo3uHHl1_tgfvGU6bvEApHldwL5Cfi-jW81XmVSsoSzVffYYh4PKIR05mxtiCamzxW8VX9qdfArr_9KDfBv9vvjdu05uMkj-htbatfBOLE8P4n_t1sTXZyTQaVkWTbvKvFIkZskdP-yO_jwe1TNlSHjSh8b5l8Jx0QitiiioLx85Qx8JQ2LEiVeLLaDROxHRXZfFAGfJfmVIj9J3oBE9PZ9QH5VhTI2YCNqpRP3f7M9-D3fizlQu8dkWeRfrP8GWFj2iTW_MEHg-KLfsxC9z8Xh-vNAsUvRXN6G2ZiEYk68q_DRHMQY8_tQaY9RdR7nQ2d3kdJ-DJ7xOreTzxMMCJ8rkXIu2WIux4rffRpltxe-y1gWI8G0EVYuqJdVwly9mM7OlHI7I71oqnxRYS9WqJeIxorbr70xFr2ReeZHqd8G8VDOFTZIxSxTZTzLcI-kdYA66sWiPlDLhGVJJWwrmviPQ9YWk8nadaiWky_sdixkw8GiCCfficSC6JdQatN0-SRONlqbIg1AT9eOhq7V3HzCMLWgvDM1F2vEM1cYFuN9hnXfx63eQ1tLia_B1IMF0bCLGmBCaviOszSb0kuC6eU5ZGJ071qTQ2d8-ODpu3kjZoGXiEPHvjddDB9vifUWI7BV_Gk8ca-ilbe2B7mWFq9ZkVvzRtJ0xwwW1bl8Dokk2n3pWLdE_S1RN73GVdyChQG345ewp8ukjCya7pSyjiq_gOnwtdjStqc1bNAew--nM3E406Czg0ATZMAFn0pmu102GdE2eWhrLybzSqvOEc8n8LJlq0g9L06bbtIfD1acv21OyvLk6kb3Bp4QtCYpT6PzDLZP8n5Wwf1dc7w7blCXcsuzZEWPC3y_UFf1RZVbQ1XWj538TKM7PF89WkDG98-hu9laucfd3RVqao5fpSe-Wbjqw7qfxcfkGDMyu3cbVWXO9m55ThBaeQQnC1p6BKiBVOuHh24fsocHLV3fvzqlVlPJC-zjYfase-fr9IyuxdWfNefEhnpyj9y7N_rcsOeFaGOgxmdovBYUBqbjPhQPrSvL-LHbrifzqzQld_3GOLGNUt_Npe40zarJpFyJOb905oi60SsEslMv7oOYuA9v_Cl5aJhHZhMEX7B9-BPcjtMmMb4frf8Hm6bNOA== \ No newline at end of file diff --git a/examples/pki/cms/auth_token_scoped.json b/examples/pki/cms/auth_token_scoped.json index 16eb644fb..4ea301694 100644 --- a/examples/pki/cms/auth_token_scoped.json +++ b/examples/pki/cms/auth_token_scoped.json @@ -1 +1,88 @@ -{"access": {"serviceCatalog": [{"endpoints": [{"adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "volume", "name": "volume"}, {"endpoints": [{"adminURL": "http://127.0.0.1:9292/v1", "region": "regionOne", "internalURL": "http://127.0.0.1:9292/v1", "publicURL": "http://127.0.0.1:9292/v1"}], "endpoints_links": [], "type": "image", "name": "glance"}, {"endpoints": [{"adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "compute", "name": "nova"}, {"endpoints": [{"adminURL": "http://127.0.0.1:35357/v2.0", "region": "RegionOne", "internalURL": "http://127.0.0.1:35357/v2.0", "publicURL": "http://127.0.0.1:5000/v2.0"}], "endpoints_links": [], "type": "identity", "name": "keystone"}],"token": {"expires": "2012-06-02T14:47:34Z", "id": "placeholder", "tenant": {"enabled": true, "description": null, "name": "tenant_name1", "id": "tenant_id1"}}, "user": {"username": "user_name1", "roles_links": ["role1","role2"], "id": "user_id1", "roles": [{"name": "role1"}, {"name": "role2"}], "name": "user_name1"}}} +{ + "access": { + "token": { + "expires": "2038-01-18T21:14:07Z", + "issued_at": "2002-01-18T21:14:07Z", + "id": "placeholder", + "tenant": { + "id": "tenant_id1", + "enabled": true, + "description": null, + "name": "tenant_name1" + } + }, + "serviceCatalog": [ + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne", + "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a" + } + ], + "type": "volume", + "name": "volume" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:9292/v1", + "region": "regionOne", + "internalURL": "http://127.0.0.1:9292/v1", + "publicURL": "http://127.0.0.1:9292/v1" + } + ], + "type": "image", + "name": "glance" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne", + "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a" + } + ], + "type": "compute", + "name": "nova" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:35357/v2.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:35357/v2.0", + "publicURL": "http://127.0.0.1:5000/v2.0" + } + ], + "type": "identity", + "name": "keystone" + } + ], + "user": { + "username": "user_name1", + "roles_links": [ + "role1", + "role2" + ], + "id": "user_id1", + "roles": [ + { + "id": "f03fda8f8a3249b2a70fb1f176a7b631", + "name": "role1" + }, + { + "id": "f03fda8f8a3249b2a70fb1f176a7b631", + "name": "role2" + } + ], + "name": "user_name1" + } + } +} diff --git a/examples/pki/cms/auth_token_scoped.pem b/examples/pki/cms/auth_token_scoped.pem index 529d7f818..2974358ba 100644 --- a/examples/pki/cms/auth_token_scoped.pem +++ b/examples/pki/cms/auth_token_scoped.pem @@ -1,41 +1,79 @@ -----BEGIN CMS----- -MIIHQAYJKoZIhvcNAQcCoIIHMTCCBy0CAQExCTAHBgUrDgMCGjCCBc4GCSqGSIb3 -DQEHAaCCBb8EggW7eyJhY2Nlc3MiOiB7InNlcnZpY2VDYXRhbG9nIjogW3siZW5k -cG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2L3Yx -LzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInJlZ2lvbiI6ICJy -ZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2 -L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInB1YmxpY1VS -TCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThh -NjBmY2Y4OWJiNjYxN2EifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUi -OiAidm9sdW1lIiwgIm5hbWUiOiAidm9sdW1lIn0sIHsiZW5kcG9pbnRzIjogW3si -YWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwgInJlZ2lvbiI6 -ICJyZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5 -MjkyL3YxIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjkyOTIvdjEi -fV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAiaW1hZ2UiLCAibmFt -ZSI6ICJnbGFuY2UifSwgeyJlbmRwb2ludHMiOiBbeyJhZG1pblVSTCI6ICJodHRw -Oi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5 -YmI2NjE3YSIsICJyZWdpb24iOiAicmVnaW9uT25lIiwgImludGVybmFsVVJMIjog -Imh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYw -ZmNmODliYjY2MTdhIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3 -NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSJ9XSwgImVu -ZHBvaW50c19saW5rcyI6IFtdLCAidHlwZSI6ICJjb21wdXRlIiwgIm5hbWUiOiAi -bm92YSJ9LCB7ImVuZHBvaW50cyI6IFt7ImFkbWluVVJMIjogImh0dHA6Ly8xMjcu -MC4wLjE6MzUzNTcvdjIuMCIsICJyZWdpb24iOiAiUmVnaW9uT25lIiwgImludGVy -bmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjIuMCIsICJwdWJsaWNV -UkwiOiAiaHR0cDovLzEyNy4wLjAuMTo1MDAwL3YyLjAifV0sICJlbmRwb2ludHNf -bGlua3MiOiBbXSwgInR5cGUiOiAiaWRlbnRpdHkiLCAibmFtZSI6ICJrZXlzdG9u -ZSJ9XSwidG9rZW4iOiB7ImV4cGlyZXMiOiAiMjAxMi0wNi0wMlQxNDo0NzozNFoi -LCAiaWQiOiAicGxhY2Vob2xkZXIiLCAidGVuYW50IjogeyJlbmFibGVkIjogdHJ1 -ZSwgImRlc2NyaXB0aW9uIjogbnVsbCwgIm5hbWUiOiAidGVuYW50X25hbWUxIiwg -ImlkIjogInRlbmFudF9pZDEifX0sICJ1c2VyIjogeyJ1c2VybmFtZSI6ICJ1c2Vy -X25hbWUxIiwgInJvbGVzX2xpbmtzIjogWyJyb2xlMSIsInJvbGUyIl0sICJpZCI6 -ICJ1c2VyX2lkMSIsICJyb2xlcyI6IFt7Im5hbWUiOiAicm9sZTEifSwgeyJuYW1l -IjogInJvbGUyIn1dLCAibmFtZSI6ICJ1c2VyX25hbWUxIn19fQ0KMYIBSTCCAUUC -AQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTES -MBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sxETAPBgNVBAsT -CEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3Jn -MRQwEgYDVQQDEwtTZWxmIFNpZ25lZAIBETAHBgUrDgMCGjANBgkqhkiG9w0BAQEF -AASBgFizBVs3dCvlHx04nUHgXHpaA9RL+e3uaaNszK9UwCBpBlv8c6+74sz6i3+G -eYDIpL9bc6QgNJ6cKhmW5yLmS8/+mmAMAcm06bdWc7p/mqC3Ild+xmQ+OHDYyyJg -DvtRUgtidFUCvxne/nwKK0WHJlpY+iwWqel5F+Xqmb8vheb1 +MIIOPwYJKoZIhvcNAQcCoIIOMDCCDiwCAQExDTALBglghkgBZQMEAgEwggxEBgkq +hkiG9w0BBwGgggw1BIIMMXsNCiAgICAiYWNjZXNzIjogew0KICAgICAgICAidG9r +ZW4iOiB7DQogICAgICAgICAgICAiZXhwaXJlcyI6ICIyMDM4LTAxLTE4VDIxOjE0 +OjA3WiIsDQogICAgICAgICAgICAiaXNzdWVkX2F0IjogIjIwMDItMDEtMThUMjE6 +MTQ6MDdaIiwNCiAgICAgICAgICAgICJpZCI6ICJwbGFjZWhvbGRlciIsDQogICAg +ICAgICAgICAidGVuYW50Ijogew0KICAgICAgICAgICAgICAgICJpZCI6ICJ0ZW5h +bnRfaWQxIiwNCiAgICAgICAgICAgICAgICAiZW5hYmxlZCI6IHRydWUsDQogICAg +ICAgICAgICAgICAgImRlc2NyaXB0aW9uIjogbnVsbCwNCiAgICAgICAgICAgICAg +ICAibmFtZSI6ICJ0ZW5hbnRfbmFtZTEiDQogICAgICAgICAgICB9DQogICAgICAg +IH0sDQogICAgICAgICJzZXJ2aWNlQ2F0YWxvZyI6IFsNCiAgICAgICAgICAgIHsN +CiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAgICAg +ICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAgew0K +ICAgICAgICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6Ly8xMjcu +MC4wLjE6ODc3Ni92MS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIs +DQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSIs +DQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDov +LzEyNy4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2 +MTdhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0 +cDovLzEyNy4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODli +YjY2MTdhIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAg +XSwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJ2b2x1bWUiLA0KICAgICAgICAg +ICAgICAgICJuYW1lIjogInZvbHVtZSINCiAgICAgICAgICAgIH0sDQogICAgICAg +ICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0K +ICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAg +ICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRw +Oi8vMTI3LjAuMC4xOjkyOTIvdjEiLA0KICAgICAgICAgICAgICAgICAgICAgICAg +InJlZ2lvbiI6ICJyZWdpb25PbmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAg +ImludGVybmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQogICAg +ICAgICAgICAgICAgICAgICAgICAicHVibGljVVJMIjogImh0dHA6Ly8xMjcuMC4w +LjE6OTI5Mi92MSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAg +ICAgIF0sDQogICAgICAgICAgICAgICAgInR5cGUiOiAiaW1hZ2UiLA0KICAgICAg +ICAgICAgICAgICJuYW1lIjogImdsYW5jZSINCiAgICAgICAgICAgIH0sDQogICAg +ICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtd +LA0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAg +ICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJo +dHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZj +Zjg5YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjog +InJlZ2lvbk9uZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxV +UkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YxLjEvNjRiNmYzZmJjYzUzNDM1 +ZThhNjBmY2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInB1 +YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNj +NTM0MzVlOGE2MGZjZjg5YmI2NjE3YSINCiAgICAgICAgICAgICAgICAgICAgfQ0K +ICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgInR5cGUiOiAiY29t +cHV0ZSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAibm92YSINCiAgICAgICAg +ICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50 +c19saW5rcyI6IFtdLA0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQog +ICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJh +ZG1pblVSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjM1MzU3L3YyLjAiLA0KICAgICAg +ICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6ICJSZWdpb25PbmUiLA0KICAgICAg +ICAgICAgICAgICAgICAgICAgImludGVybmFsVVJMIjogImh0dHA6Ly8xMjcuMC4w +LjE6MzUzNTcvdjIuMCIsDQogICAgICAgICAgICAgICAgICAgICAgICAicHVibGlj +VVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6NTAwMC92Mi4wIg0KICAgICAgICAgICAg +ICAgICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAi +dHlwZSI6ICJpZGVudGl0eSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAia2V5 +c3RvbmUiDQogICAgICAgICAgICB9DQogICAgICAgIF0sDQogICAgICAgICJ1c2Vy +Ijogew0KICAgICAgICAgICAgInVzZXJuYW1lIjogInVzZXJfbmFtZTEiLA0KICAg +ICAgICAgICAgInJvbGVzX2xpbmtzIjogWw0KICAgICAgICAgICAgICAgICJyb2xl +MSIsDQogICAgICAgICAgICAgICAgInJvbGUyIg0KICAgICAgICAgICAgXSwNCiAg +ICAgICAgICAgICJpZCI6ICJ1c2VyX2lkMSIsDQogICAgICAgICAgICAicm9sZXMi +OiBbDQogICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAiaWQi +OiAiZjAzZmRhOGY4YTMyNDliMmE3MGZiMWYxNzZhN2I2MzEiLA0KICAgICAgICAg +ICAgICAgICAgICAibmFtZSI6ICJyb2xlMSINCiAgICAgICAgICAgICAgICB9LA0K +ICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgImlkIjogImYw +M2ZkYThmOGEzMjQ5YjJhNzBmYjFmMTc2YTdiNjMxIiwNCiAgICAgICAgICAgICAg +ICAgICAgIm5hbWUiOiAicm9sZTIiDQogICAgICAgICAgICAgICAgfQ0KICAgICAg +ICAgICAgXSwNCiAgICAgICAgICAgICJuYW1lIjogInVzZXJfbmFtZTEiDQogICAg +ICAgIH0NCiAgICB9DQp9DQoxggHOMIIBygIBATCBpDCBnjEKMAgGA1UEBRMBNTEL +MAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTdW5ueXZhbGUxEjAQ +BgNVBAoMCU9wZW5TdGFjazERMA8GA1UECwwIS2V5c3RvbmUxJTAjBgkqhkiG9w0B +CQEWFmtleXN0b25lQG9wZW5zdGFjay5vcmcxFDASBgNVBAMMC1NlbGYgU2lnbmVk +AgERMAsGCWCGSAFlAwQCATANBgkqhkiG9w0BAQEFAASCAQBYn04AtTif863EsRze +H4qwhQwcDMEy9+tqm6Pof1ysl7wnqtyeJbUaEmljzYOeiYvcI9tOAj/beTpxbU3A +6vo0XkPt82Fdn3tPu2Rbr5cmCWkqRUAazEXBwfwPFBeA01Th2yxtTNUeiZJyjbcO +6DfeXeQSQWkblB375+2RQNGOuEbU1JwC8sErk8eTetA419fh+tn5m3h/FhHRiWGo +2NWy9HRx0OjCnnjNtIWY++QbVLQS7lty/f3E1c3l8ebGhBXleEmWbpp1zUi9e5oK +0NlVB+nwiw9qkzAgX5ForSaFGkGlHjjAtoIs/i3DgP+3ET/pZkislu6NpiXfklY4 +o35g -----END CMS----- diff --git a/examples/pki/cms/auth_token_scoped.pkiz b/examples/pki/cms/auth_token_scoped.pkiz new file mode 100644 index 000000000..a3177c16a --- /dev/null +++ b/examples/pki/cms/auth_token_scoped.pkiz @@ -0,0 +1 @@ +PKIZ_eJylVkmTmzgUvutXzN2VCotxm0MObMbQSG52ixtLG5DBdrttC_j1I3An6XRSk8wMVS6jJ_H0vfe97dMn9qiGaaG_NOiPi08AWpa1KbH9eEys6pYjxc21I5M9Dhp7ck1xjU4LlLVahme9hJpJNE3d56bmv5i-lYlAd421kjIhKY2yxNxzb1dYQE0uwnpTqw_WwbulQnS1yLFke6dcRHwSezu8ddm-UgNIFAprjkKf6zYrt4fBsUP6kSL-WDuaUifbiqZbu8l7a2FpVg91OHcCpXMCYx7pVgc2xOA2RBHj2nq1NPuUaONBm2bmiiRxdctMr8nve1wSS1V2cO_I2uiKY_sVJPEk4PJD1Iw3pvEdWmGOByRuKzR76E8K2JpvRlOYWU3Wrq7FSr6CUfh2YJ9sEcnbhhZmc8tqhsSU-Mzs5J1P2UfML4fkhIVIx1uvykz5MCoDsfhaM2jMr_IpO3jDKBxlOPYuaSxF4Z5OiNK1x-X68eYMRo_6OXWIcmX-mgM05IIj4s4ZMIdJ0kIhqbEAeTi4A4rDOQ4wTVrUbvSmxoTtBEVl1SMiu0mE5gYmqJrdJ3FxygTpKWvD-u4LiUu2o93dP6IAI4z_jkLlAW67E-YjP7jTdyzWHt3UyxsMLHGyU5t3G1KKaMC3ghg3RLwatXhIWpvgIRwA0iGfBFWFiNpiAc83sV0jgjskGPUu4kZ2GGUezYTmWqzRLjOba3qP0mzL2AGMUyk3wzv3rfxajFyP8FoWNPEH-YEpXP_IGviXtEmQ7PvRX1-ZACMV_4cJ8GvNKv9nzt33YBNYo3f_yGHv_ZXGfJUIYQ1GqCwxLok_3XRgWXjFbGOMf5b_7xTeFY31IjH5U9bc0YF_5t4d0V2hvxSQaQkJYRHQIoICyMEhajamIQBoJiQhpYRbS0DEEPE9M98cOp_g5m10SGP5GgjSG8UMkRn1DPkriCIbTjneVlyxVhZOv-wgyUcUjDpjsdFZEdNkAfrzX4Y6-F2s_44N8G_s_dlcWwYTPay-JWv1NgZOzsuv7P88FdHVpRhZKtYNfWOJZAJPi633LdzB13jPWlkYNTravWB-U3hXxGSrfRY3148-Ax-dBlmKoiBn5lhM9jMj4QdGwHtKfsfIL5RTULDansbod1nIQ12hLFd6tv4h7MGfxT3rAwfvVKz39YfQP4Nk2wyFKV8T5sD7h9GQbK23vji-v28o03o3KQiMSRnIWbVhDeUHBKxQsJYWfi0a43tvNdz71sfnQtSPXQu8daU-E7rmO2XN_u5MTFnY7nFQtSyQBkhcCRO7QgOrn2TVwiAXAA4KVkRh97EOTsgYzLe0_npzC3XUJqYxT0hVwcHiwCa2ehzkLBesLmHhiVoWoqxg_9xQ30w58MV7R4Lv9ky3d-yAvAtMTcmPtCzXplIaKrTMPfs9Q_dINQXrkeuuDGrw0H2lQHMngWlQOwoHw4HK3lT40NBUqLmc0RlEcdUSRaqSB1qE-KyVpIIFHTPPh6pigulwBe1AVJusQRyO0Rl6BtXppNgxaOV8ozowGqjBb_MRG49soHg4ZjOQlIveLWsjJRsVHe6KnFbuk8EIoWpNqJQOOqEQvSa1GqRxcWXDicYUGFSlePVI57pSHanuvp_YDFV1FTZ8GcrR8fnhBZ6Ka4w_z1waumEnBQICw9PlSQwpmuOXQEtDY1bOTjj9LPl8uh4cdbkwrm2OWkvkdNecc8q88Qq03ivo7FKXPokgTtSDB88PHJnrPsGz2usVbDw-n8O63D4f3dNgPKk7a_biR4EmIrWSU4srF7vhtnDD54cdmMmZX1mv-7amy94ZxKBOLw9pcsj4gX19SR_oSrDzM5JO9SyfyVJcbZ6e02A2S-OLTC4FkJf-jddP9bBYPl1m5Voqs3WnWhDXnVq-SMre4j9_3qsCryp28xwfnzDPZaQ4s5GkWyzkGNyiy9Z9sG_4Obsttd3jUhXFonIWc_9Y53m3ibLSDg2P1fIjuZ1Z3WnDTVPBLjy2p8H98gVME7OB9O_T89-mTsWe \ No newline at end of file diff --git a/examples/pki/cms/auth_token_scoped_expired.json b/examples/pki/cms/auth_token_scoped_expired.json new file mode 100644 index 000000000..d9ea0e92f --- /dev/null +++ b/examples/pki/cms/auth_token_scoped_expired.json @@ -0,0 +1,88 @@ +{ + "access": { + "token": { + "expires": "2010-06-02T14:47:34Z", + "issued_at": "2002-01-18T21:14:07Z", + "id": "placeholder", + "tenant": { + "id": "tenant_id1", + "enabled": true, + "description": null, + "name": "tenant_name1" + } + }, + "serviceCatalog": [ + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne", + "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a" + } + ], + "type": "volume", + "name": "volume" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:9292/v1", + "region": "regionOne", + "internalURL": "http://127.0.0.1:9292/v1", + "publicURL": "http://127.0.0.1:9292/v1" + } + ], + "type": "image", + "name": "glance" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne", + "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a" + } + ], + "type": "compute", + "name": "nova" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://127.0.0.1:35357/v2.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:35357/v2.0", + "publicURL": "http://127.0.0.1:5000/v2.0" + } + ], + "type": "identity", + "name": "keystone" + } + ], + "user": { + "username": "user_name1", + "roles_links": [ + "role1", + "role2" + ], + "id": "user_id1", + "roles": [ + { + "id": "f03fda8f8a3249b2a70fb1f176a7b631", + "name": "role1" + }, + { + "id": "f03fda8f8a3249b2a70fb1f176a7b631", + "name": "role2" + } + ], + "name": "user_name1" + } + } +} diff --git a/examples/pki/cms/auth_token_scoped_expired.pem b/examples/pki/cms/auth_token_scoped_expired.pem new file mode 100644 index 000000000..9c4bdb9e1 --- /dev/null +++ b/examples/pki/cms/auth_token_scoped_expired.pem @@ -0,0 +1,79 @@ +-----BEGIN CMS----- +MIIOPwYJKoZIhvcNAQcCoIIOMDCCDiwCAQExDTALBglghkgBZQMEAgEwggxEBgkq +hkiG9w0BBwGgggw1BIIMMXsNCiAgICAiYWNjZXNzIjogew0KICAgICAgICAidG9r +ZW4iOiB7DQogICAgICAgICAgICAiZXhwaXJlcyI6ICIyMDEwLTA2LTAyVDE0OjQ3 +OjM0WiIsDQogICAgICAgICAgICAiaXNzdWVkX2F0IjogIjIwMDItMDEtMThUMjE6 +MTQ6MDdaIiwNCiAgICAgICAgICAgICJpZCI6ICJwbGFjZWhvbGRlciIsDQogICAg +ICAgICAgICAidGVuYW50Ijogew0KICAgICAgICAgICAgICAgICJpZCI6ICJ0ZW5h +bnRfaWQxIiwNCiAgICAgICAgICAgICAgICAiZW5hYmxlZCI6IHRydWUsDQogICAg +ICAgICAgICAgICAgImRlc2NyaXB0aW9uIjogbnVsbCwNCiAgICAgICAgICAgICAg +ICAibmFtZSI6ICJ0ZW5hbnRfbmFtZTEiDQogICAgICAgICAgICB9DQogICAgICAg +IH0sDQogICAgICAgICJzZXJ2aWNlQ2F0YWxvZyI6IFsNCiAgICAgICAgICAgIHsN +CiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAgICAg +ICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAgew0K +ICAgICAgICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6Ly8xMjcu +MC4wLjE6ODc3Ni92MS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIs +DQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSIs +DQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDov +LzEyNy4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2 +MTdhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0 +cDovLzEyNy4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODli +YjY2MTdhIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAg +XSwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJ2b2x1bWUiLA0KICAgICAgICAg +ICAgICAgICJuYW1lIjogInZvbHVtZSINCiAgICAgICAgICAgIH0sDQogICAgICAg +ICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtdLA0K +ICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAg +ICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJodHRw +Oi8vMTI3LjAuMC4xOjkyOTIvdjEiLA0KICAgICAgICAgICAgICAgICAgICAgICAg +InJlZ2lvbiI6ICJyZWdpb25PbmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAg +ImludGVybmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQogICAg +ICAgICAgICAgICAgICAgICAgICAicHVibGljVVJMIjogImh0dHA6Ly8xMjcuMC4w +LjE6OTI5Mi92MSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAg +ICAgIF0sDQogICAgICAgICAgICAgICAgInR5cGUiOiAiaW1hZ2UiLA0KICAgICAg +ICAgICAgICAgICJuYW1lIjogImdsYW5jZSINCiAgICAgICAgICAgIH0sDQogICAg +ICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50c19saW5rcyI6IFtd +LA0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAg +ICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJhZG1pblVSTCI6ICJo +dHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZj +Zjg5YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjog +InJlZ2lvbk9uZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJuYWxV +UkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YxLjEvNjRiNmYzZmJjYzUzNDM1 +ZThhNjBmY2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInB1 +YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNj +NTM0MzVlOGE2MGZjZjg5YmI2NjE3YSINCiAgICAgICAgICAgICAgICAgICAgfQ0K +ICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgInR5cGUiOiAiY29t +cHV0ZSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAibm92YSINCiAgICAgICAg +ICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50 +c19saW5rcyI6IFtdLA0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQog +ICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJh +ZG1pblVSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjM1MzU3L3YyLjAiLA0KICAgICAg +ICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6ICJSZWdpb25PbmUiLA0KICAgICAg +ICAgICAgICAgICAgICAgICAgImludGVybmFsVVJMIjogImh0dHA6Ly8xMjcuMC4w +LjE6MzUzNTcvdjIuMCIsDQogICAgICAgICAgICAgICAgICAgICAgICAicHVibGlj +VVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6NTAwMC92Mi4wIg0KICAgICAgICAgICAg +ICAgICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAi +dHlwZSI6ICJpZGVudGl0eSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAia2V5 +c3RvbmUiDQogICAgICAgICAgICB9DQogICAgICAgIF0sDQogICAgICAgICJ1c2Vy +Ijogew0KICAgICAgICAgICAgInVzZXJuYW1lIjogInVzZXJfbmFtZTEiLA0KICAg +ICAgICAgICAgInJvbGVzX2xpbmtzIjogWw0KICAgICAgICAgICAgICAgICJyb2xl +MSIsDQogICAgICAgICAgICAgICAgInJvbGUyIg0KICAgICAgICAgICAgXSwNCiAg +ICAgICAgICAgICJpZCI6ICJ1c2VyX2lkMSIsDQogICAgICAgICAgICAicm9sZXMi +OiBbDQogICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAiaWQi +OiAiZjAzZmRhOGY4YTMyNDliMmE3MGZiMWYxNzZhN2I2MzEiLA0KICAgICAgICAg +ICAgICAgICAgICAibmFtZSI6ICJyb2xlMSINCiAgICAgICAgICAgICAgICB9LA0K +ICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgImlkIjogImYw +M2ZkYThmOGEzMjQ5YjJhNzBmYjFmMTc2YTdiNjMxIiwNCiAgICAgICAgICAgICAg +ICAgICAgIm5hbWUiOiAicm9sZTIiDQogICAgICAgICAgICAgICAgfQ0KICAgICAg +ICAgICAgXSwNCiAgICAgICAgICAgICJuYW1lIjogInVzZXJfbmFtZTEiDQogICAg +ICAgIH0NCiAgICB9DQp9DQoxggHOMIIBygIBATCBpDCBnjEKMAgGA1UEBRMBNTEL +MAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTdW5ueXZhbGUxEjAQ +BgNVBAoMCU9wZW5TdGFjazERMA8GA1UECwwIS2V5c3RvbmUxJTAjBgkqhkiG9w0B +CQEWFmtleXN0b25lQG9wZW5zdGFjay5vcmcxFDASBgNVBAMMC1NlbGYgU2lnbmVk +AgERMAsGCWCGSAFlAwQCATANBgkqhkiG9w0BAQEFAASCAQCFiZDCR4kvDWvNVo3l +306T6uMi+CuLklTr9msrA4rhRDROZ6N8y/0TrpnBInhJC9A5OGnRgiAPFrg3ksLP +ONqqtdQSgNLnzVjauJzVvejzOIYtnp6quQzxy+B1xS/QX+8ODgxz8PcvFszWDvBx +qDi/q3XAU2fvdRkq96WBGkqjAOb1pxHA1WbOklcjAm/PL5qgcFc+aryiVvPVBjFy +1KfOZjncXtDBB0Bz5+7MxAxej7LhRZ/eqlK2A/mn2vIlvPKLTGdfuQ10aIBtJ5lW +cP2miCjk179e2OU71eJdpJk1bFBXNoNbeu7dhI6/W65SKo/EbEgLO07NXW4qqcVQ +vnt9 +-----END CMS----- diff --git a/examples/pki/cms/auth_token_scoped_expired.pkiz b/examples/pki/cms/auth_token_scoped_expired.pkiz new file mode 100644 index 000000000..5824b4efb --- /dev/null +++ b/examples/pki/cms/auth_token_scoped_expired.pkiz @@ -0,0 +1 @@ +PKIZ_eJylVtuSmzgQfddX7PtUKiDsGfOQBy4yhrHEcLd4MzAGZLA99pjb16_Ak2QySW2yu66iDC3RnO5zulufPvGfigyT_KVhb3z4BLBpmnZOrcdjbBZNShQn1Y7c9jho_JdqioM6zVdWah6c9RxrBtM0dZ8amvdieGYiAd1BK2XLjSxHeU6F594qKCRVKuHSLtUH8-A2WxheTXbM-doplYgYR-6Obhy-rpQAM6XFpdBiT-jspdNj_9gR_dgS8ViuNaWMN0W73VhV2pv3pmb2WEft2lcgv_pQRwKwmSPZDAtRaV5MzTrF2rjRahNjyeKoaBLDrdLbmhBH8yI5ODdkdXilkXUBcTQZhPQQVuMXt9ENWmaMG-bCBlZ77E0O-LNYjaHwsKqkXl6zpXwFo_Ftwz7eEJbWVZsZVZOUHIkxFxOjk3dey1_ieTnEJwpDnW7cIjHkw-gMRNKl5NB4XuVTcnCH0TjaaOS-bqN5GOzbCdF25QqpfmzWA-pJP2vXTLnyfM0AGVK4lmi3HqhAWVxjGJcUYhEPzkCiYEZ92sY1qW29KinjK35WmOWIyKpiWDVggqpZfRxlpwTOn5I6KG-5mAvxZoy7-0cUYITx31GoIqB1d6Ji6Pk3-o7Zym3tctFg35SmOLVZZ7NcIgNtMoYawtyS1HSIa4vRIRgA0bEY-0VBmFpTSGd2ZJWE0Y5AVO5CYWSHU-a2Cayu2YrsEqO6bm8qTTacHcA5nadGcOO-li_ZyPUIr-aiiT7YD9zh6kfWwL-kbY7Zvh_z9ZUJMFLxf5gAv_asin-W3H0PbN8cs_tHCXufr20kFjEMSjBC5YXxGnvTlw68Cq-UL4z65_X_zuHN0dgvYkM8JdUNHfhn7p0R3RV7C0gME8aMK6AmjPhYwENY2QaCABsxi1k-p7UJCUMSvVXmW0JnE9y0Dg_bSL76cP5GMUdkhD1HfgFhaOGpxutCyFbK_bpfdJilIwpOHbq3dd7ENBlib_ZLqYPfaf13bIB_E-_P4VoymOjh_S1eqc0onFSUL_z_PDXR5Ws2spStqvaNJZZAsc027je5g696T2oZjh7X2q1hfnN4c8Rty30SVdePOQMfk4Z5iRI_5eGY3PYzI8EHRsB7Sn7HyC-ctyDjvX0bkd9VoYh1peW10vPnH2QP_kz3fA4c3FO22pcfpH8G8aYaMkO-xjyBtxfDId6Yb3NxvH8_UKbn3eTAR5MzkPJuwwfKDwh4o-AjLfjaNMb73qyE96NPTGHYj1MLvE2lPoFd9Z2yan9LJm25bPfUL2oupAEzZ06ZVZCB90-2rLGfQkD9jDdR3H3sgxMyDvOtrL9-ucY6qWMDzWJWFHgw-XSOzJ76Ka8Fs4u5PEnNJcob9s8D9S2Ug5i9TyT4Hs_09Y5vkHe-oSnpsc3zlaHkSMWmsefXM3aOraZQPXScJWqRiJ1LCzRnMhiotcJgQGus7A1FDJCmYs0RUIeY4qg5CVUl9bWQiEk9n2dcdDw8D6uKAabNBbZ8Sa2Sigg0ImfsolZvJ8dr1Bbrb1T7qMIa_nY-4scjCygujfgZaJ5KbpPUoZKMjg43R-ta7uMBBVg1J1RKh9cBDC9xqfrbKLvyw4nGHaBWbenysZ3pSnFsdef9iQ2pqqPwwxdShifbKSSRDulneAkzL_1s95acEXAHC8PNXUdeP_Qrz9qeXvW6znWfvOrq4irIun8XtKKPVnfijJEHMs9DT9RWUmE9KjynDFjbJfPxS7Mx0iWWFs9RWj46L3Z3V587XM1PWwNCXoTJ2UNDv1d9vvuz9tLXV_kxDWYYgqHYd8_N6XzoH2b-xWpw0y_gJtAsxErjRb5G4RbKz_YBO2VeLXPDpyxSbVs8N2ZTBVgCiztMAyErXrdbb7N_Me8fcjVnq9dBsKTF4alod0-SuyzE3LNe81ZdFU6TCv48WWXN-hB7O_B8DmemaSyebLh7CO6kaJFerYooCIa2zek7UjVGq6ck30Okq4_3s0OV2WpyLwkwwsqXL2A6MSOifz89_w1E1sSW \ No newline at end of file diff --git a/examples/pki/cms/auth_token_unscoped.json b/examples/pki/cms/auth_token_unscoped.json index b2340a764..844cebd5d 100644 --- a/examples/pki/cms/auth_token_unscoped.json +++ b/examples/pki/cms/auth_token_unscoped.json @@ -1 +1,26 @@ -{"access": {"token": {"expires": "2012-08-17T15:35:34Z", "id": "01e032c996ef4406b144335915a41e79"}, "serviceCatalog": {}, "user": {"username": "user_name1", "roles_links": [], "id": "c9c89e3be3ee453fbf00c7966f6d3fbd", "roles": [{'name': 'role1'},{'name': 'role2'},], "name": "user_name1"}}} \ No newline at end of file +{ + "access": { + "token": { + "expires": "2112-08-17T15:35:34Z", + "issued_at": "2002-01-18T21:14:07Z", + "id": "01e032c996ef4406b144335915a41e79" + }, + "serviceCatalog": {}, + "user": { + "username": "user_name1", + "roles_links": [], + "id": "c9c89e3be3ee453fbf00c7966f6d3fbd", + "roles": [ + { + "id": "359da42d31c04437a32812aeb79e9c0b", + "name": "role1" + }, + { + "id": "581af19726fa4af5bda745789ab2bf2b", + "name": "role2" + } + ], + "name": "user_name1" + } + } +} diff --git a/examples/pki/cms/auth_token_unscoped.pem b/examples/pki/cms/auth_token_unscoped.pem index d42d1f17f..bfb97968f 100644 --- a/examples/pki/cms/auth_token_unscoped.pem +++ b/examples/pki/cms/auth_token_unscoped.pem @@ -1,17 +1,29 @@ -----BEGIN CMS----- -MIICpwYJKoZIhvcNAQcCoIICmDCCApQCAQExCTAHBgUrDgMCGjCCATUGCSqGSIb3 -DQEHAaCCASYEggEieyJhY2Nlc3MiOiB7InRva2VuIjogeyJleHBpcmVzIjogIjIw -MTItMDgtMTdUMTU6MzU6MzRaIiwgImlkIjogIjAxZTAzMmM5OTZlZjQ0MDZiMTQ0 -MzM1OTE1YTQxZTc5In0sICJzZXJ2aWNlQ2F0YWxvZyI6IHt9LCAidXNlciI6IHsi -dXNlcm5hbWUiOiAidXNlcl9uYW1lMSIsICJyb2xlc19saW5rcyI6IFtdLCAiaWQi -OiAiYzljODllM2JlM2VlNDUzZmJmMDBjNzk2NmY2ZDNmYmQiLCAicm9sZXMiOiBb -eyduYW1lJzogJ3JvbGUxJ30seyduYW1lJzogJ3JvbGUyJ30sXSwgIm5hbWUiOiAi -dXNlcl9uYW1lMSJ9fX0xggFJMIIBRQIBATCBpDCBnjEKMAgGA1UEBRMBNTELMAkG -A1UEBhMCVVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxEjAQBgNV -BAoTCU9wZW5TdGFjazERMA8GA1UECxMIS2V5c3RvbmUxJTAjBgkqhkiG9w0BCQEW -FmtleXN0b25lQG9wZW5zdGFjay5vcmcxFDASBgNVBAMTC1NlbGYgU2lnbmVkAgER -MAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIGAITCwkW7cAbcWbCBD5GfGMGHB9hP/ -UagaCZ8HFhlzjdQoJjvC+Mtu+3lWlwqPGR8ztY9kBc1401S2qJxD4FGo+M3CkNpF -s0mtaT2PUJfFkDCzHqeBQNFHyZeqLjkPYnokPcw4s3i60DBGTFfAiUT3xumn8a4h -C+zEAee35C/A+Iw= +MIIE/gYJKoZIhvcNAQcCoIIE7zCCBOsCAQExDTALBglghkgBZQMEAgEwggMDBgkq +hkiG9w0BBwGgggL0BIIC8HsNCiAgICAiYWNjZXNzIjogew0KICAgICAgICAidG9r +ZW4iOiB7DQogICAgICAgICAgICAiZXhwaXJlcyI6ICIyMTEyLTA4LTE3VDE1OjM1 +OjM0WiIsDQogICAgICAgICAgICAiaXNzdWVkX2F0IjogIjIwMDItMDEtMThUMjE6 +MTQ6MDdaIiwNCiAgICAgICAgICAgICJpZCI6ICIwMWUwMzJjOTk2ZWY0NDA2YjE0 +NDMzNTkxNWE0MWU3OSINCiAgICAgICAgfSwNCiAgICAgICAgInNlcnZpY2VDYXRh +bG9nIjoge30sDQogICAgICAgICJ1c2VyIjogew0KICAgICAgICAgICAgInVzZXJu +YW1lIjogInVzZXJfbmFtZTEiLA0KICAgICAgICAgICAgInJvbGVzX2xpbmtzIjog +W10sDQogICAgICAgICAgICAiaWQiOiAiYzljODllM2JlM2VlNDUzZmJmMDBjNzk2 +NmY2ZDNmYmQiLA0KICAgICAgICAgICAgInJvbGVzIjogWw0KICAgICAgICAgICAg +ICAgIHsNCiAgICAgICAgICAgICAgICAgICAgImlkIjogIjM1OWRhNDJkMzFjMDQ0 +MzdhMzI4MTJhZWI3OWU5YzBiIiwNCiAgICAgICAgICAgICAgICAgICAgIm5hbWUi +OiAicm9sZTEiDQogICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICB7 +DQogICAgICAgICAgICAgICAgICAgICJpZCI6ICI1ODFhZjE5NzI2ZmE0YWY1YmRh +NzQ1Nzg5YWIyYmYyYiIsDQogICAgICAgICAgICAgICAgICAgICJuYW1lIjogInJv +bGUyIg0KICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIF0sDQogICAgICAg +ICAgICAibmFtZSI6ICJ1c2VyX25hbWUxIg0KICAgICAgICB9DQogICAgfQ0KfQ0K +MYIBzjCCAcoCAQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYD +VQQIDAJDQTESMBAGA1UEBwwJU3Vubnl2YWxlMRIwEAYDVQQKDAlPcGVuU3RhY2sx +ETAPBgNVBAsMCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVu +c3RhY2sub3JnMRQwEgYDVQQDDAtTZWxmIFNpZ25lZAIBETALBglghkgBZQMEAgEw +DQYJKoZIhvcNAQEBBQAEggEAYIiuAmAxllZ5FdGlJWu0bxAsxwFb114lZAq2/mju +v2Hu9505N4TbkJJI++ojNwv1extCAL0H7jmM2nbPovPa0R6RVDRgh1R03E+uLNld +rBXyiTeZh2OrbmoXdHAXdJRnBkLIfjDXISY5qN/yJhsr3g6djekGnkWzZfwtwjyb +qBbVVB9oK9p5svd85jiOcGlcBxuBhWeqwZiYTzyDtJ2SuwtvBaIDQICyS9VtGI+F +NzqtdUHw/vxsQqOq6tR4Qf32wtZnUS+komQWX3uJXwYpSZHomuGPbs0XolGXYm8D +fM/7R9AH6CUlmBmq/WVQdEMMv9VADaP9yHuGvM8w3f6ZvA== -----END CMS----- diff --git a/examples/pki/cms/auth_token_unscoped.pkiz b/examples/pki/cms/auth_token_unscoped.pkiz new file mode 100644 index 000000000..34f93f46f --- /dev/null +++ b/examples/pki/cms/auth_token_unscoped.pkiz @@ -0,0 +1 @@ +PKIZ_eJxdVNmWqjoQfecr7nuvs5pBW30MIWKiCSKT5A1QZqQdEOTrb7DPHVmLIVWVqr03Vfn1S1w6MjH7A1JnWvySKMZGa4dk23KcPxMG7AS2wlaVEILZDAIbDdAFGz3zbkZGoTnZo5kJnavp4FiTDBttQCSMfImyzIzPL5KHKqsTjRZWoS_w5fCMVL_DZZsJ33eiMYUHhzQ82sIPComWoKeF3FNHHqy1_aJuOzCj7ZnSFjsICn7M--hI6uSFvzDEwo9eOxfMdi7SfAMpklVSRdxyUOA7huSbw3dgTwOvpyMpLbdSeRDKzABqWCLxpiNzq4EFSBYxmmQ5ZDVVSlT_dWrqknssP5nre6wmbwqp02f44o_8iH9Tmr5JFwZKPdGSfhvSuFk_uIvesJNmdedHlsZm3UU_WsTHKVFTV9Mm3NB5OGZz7rJCEo-au7ZCVV5woUc4JnNW8oY19sgbUuFiQkCesemP0-ZAuxdR8CMgHb25xE3BRQTTgPbMsEemopGW2UCbdR2WiahSl9TEb2RvlM6kEXnF6lBTQV_aQcHrL2ilN6PBuqFupVGBInQPOS_9QhTRmOFpllHnYUkEUlK8kTXzXIoD7w3nzdvFRerL09_4W6T_a5QelRUNsf6aGioJoSQ6rc8iu8_4bIAlwHrGfB14LnC9AY6A_KxDF9S-S-17D-3Q8G0bo54YtoscierABIqH9IEST_O7-FKrYSD4HXCPwDt4i_p6n5h-52kH0aX3Ablg_5P47koQPerzkcmxOheieD3u_z0Xlb7O-Y0f6_Fkrjru6c8pUfKTqIs1cpHowe5R9q5koP7h8mBo8Jp9c5GQC0boP4MEmJ5V17wqzFUv64L-WgLAEROnxy40lpf0Fg28fjkQr5bP1g0-Qt-trp_4RXab1bDZlQvFPkffLGmr62wLSLTYS9b2MhKrIzeowvOwgP35VOcfVbq7nLEefJzqb1nfpgf1dh0sdWM-QmW3WJwrvzBhh9bFl0R2kU3U3eqYwodf6ddV2OfzzAwWt0fqJI62dwS7fUn0wd2q22tM5LzxcyqjjMNZt46kpJtbY5PjW218jXuv1s3ncAZhvFAeymKFu2I8xOuxSZf-vH76-_IKeLy0WKvq7Hgbv6A0dJ-seV45vZufmqwrsZ68hlxvo2ZBFrkV55blvzbp_nOZmZes3LycHQkPVlzKz9N2OXxJS00ui6s_aPNNIDjeWsvY-vuP9mOuIel96iFm_HMC_gm2VKmF \ No newline at end of file diff --git a/examples/pki/cms/auth_v3_token_revoked.json b/examples/pki/cms/auth_v3_token_revoked.json new file mode 100644 index 000000000..f2ddf29c2 --- /dev/null +++ b/examples/pki/cms/auth_v3_token_revoked.json @@ -0,0 +1,140 @@ +{ + "token": { + "catalog": [ + { + "endpoints": [ + { + "id": "3b5e554bcf114f2483e8a1be7a0506d1", + "interface": "admin", + "url": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + }, + { + "id": "54abd2dc463c4ba4a72915498f8ecad1", + "interface": "internal", + "url": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + }, + { + "id": "70a7efa4b1b941968357cc43ae1419ee", + "interface": "public", + "url": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + } + ], + "id": "5707c3fc0a294703a3c638e9cf6a6c3a", + "type": "volume", + "name": "volume" + }, + { + "endpoints": [ + { + "id": "92217a3b95394492859bc49fd474382f", + "interface": "admin", + "url": "http://127.0.0.1:9292/v1", + "region": "regionOne" + }, + { + "id": "f20563bdf66f4efa8a1f11d99b672be1", + "interface": "internal", + "url": "http://127.0.0.1:9292/v1", + "region": "regionOne" + }, + { + "id": "375f9ba459a447738fb60fe5fc26e9aa", + "interface": "public", + "url": "http://127.0.0.1:9292/v1", + "region": "regionOne" + } + ], + "id": "15c21aae6b274a8da52e0a068e908aac", + "type": "image", + "name": "glance" + }, + { + "endpoints": [ + { + "id": "edbd9f50f66746ae9ed11dc3b1ae35da", + "interface": "admin", + "url": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + }, + { + "id": "9e03c46c80a34a159cb39f5cb0498b92", + "interface": "internal", + "url": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + }, + { + "id": "1df0b44d92634d59bd0e0d60cf7ce432", + "interface": "public", + "url": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + } + ], + "id": "2f404fdb89154c589efbc10726b029ec", + "type": "compute", + "name": "nova" + }, + { + "endpoints": [ + { + "id": "a4501e141a4b4e14bf282e7bffd81dc5", + "interface": "admin", + "url": "http://127.0.0.1:35357/v3", + "region": "RegionOne" + }, + { + "id": "3d17e3227bfc4483b58de5eaa584e360", + "interface": "internal", + "url": "http://127.0.0.1:35357/v3", + "region": "RegionOne" + }, + { + "id": "8cd4b957090f4ca5842a22e9a74099cd", + "interface": "public", + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne" + } + ], + "id": "c5d926d566424e4fba4f80c37916cde5", + "type": "identity", + "name": "keystone" + } + ], + "issued_at": "2002-01-18T21:14:07Z", + "expires_at": "2038-01-18T21:14:07Z", + "audit_ids": ["ZzzZ2ZZYqT8OzfUVvrjEITQ", "cCCCCCctTzO1-XUk5STybw"], + "project": { + "enabled": true, + "description": null, + "name": "tenant_name1", + "id": "tenant_id1", + "domain": { + "id": "domain_id1", + "name": "domain_name1" + } + }, + "user": { + "name": "revoked_username1", + "id": "revoked_user_id1", + "domain": { + "id": "domain_id1", + "name": "domain_name1" + } + }, + "roles": [ + { + "id": "f03fda8f8a3249b2a70fb1f176a7b631", + "name": "role1" + }, + { + "id": "f03fda8f8a3249b2a70fb1f176a7b631", + "name": "role2" + } + ], + "methods": [ + "password" + ] + } +} diff --git a/examples/pki/cms/auth_v3_token_revoked.pem b/examples/pki/cms/auth_v3_token_revoked.pem new file mode 100644 index 000000000..0b1ecbf4a --- /dev/null +++ b/examples/pki/cms/auth_v3_token_revoked.pem @@ -0,0 +1,125 @@ +-----BEGIN CMS----- +MIIW/gYJKoZIhvcNAQcCoIIW7zCCFusCAQExDTALBglghkgBZQMEAgEwghUDBgkq +hkiG9w0BBwGgghT0BIIU8HsNCiAgICAidG9rZW4iOiB7DQogICAgICAgICJjYXRh +bG9nIjogWw0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICJlbmRwb2lu +dHMiOiBbDQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAg +ICAgICAgICJpZCI6ICIzYjVlNTU0YmNmMTE0ZjI0ODNlOGExYmU3YTA1MDZkMSIs +DQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJmYWNlIjogImFkbWluIiwN +CiAgICAgICAgICAgICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAu +MTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwNCiAg +ICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25lIg0KICAg +ICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgICB7DQogICAg +ICAgICAgICAgICAgICAgICAgICAiaWQiOiAiNTRhYmQyZGM0NjNjNGJhNGE3Mjkx +NTQ5OGY4ZWNhZDEiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFj +ZSI6ICJpbnRlcm5hbCIsDQogICAgICAgICAgICAgICAgICAgICAgICAidXJsIjog +Imh0dHA6Ly8xMjcuMC4wLjE6ODc3Ni92MS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZj +Zjg5YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjog +InJlZ2lvbk9uZSINCiAgICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAg +ICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAgICAgImlkIjogIjcwYTdl +ZmE0YjFiOTQxOTY4MzU3Y2M0M2FlMTQxOWVlIiwNCiAgICAgICAgICAgICAgICAg +ICAgICAgICJpbnRlcmZhY2UiOiAicHVibGljIiwNCiAgICAgICAgICAgICAgICAg +ICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2L3YxLzY0YjZmM2Zi +Y2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwNCiAgICAgICAgICAgICAgICAgICAg +ICAgICJyZWdpb24iOiAicmVnaW9uT25lIg0KICAgICAgICAgICAgICAgICAgICB9 +DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAiaWQiOiAiNTcw +N2MzZmMwYTI5NDcwM2EzYzYzOGU5Y2Y2YTZjM2EiLA0KICAgICAgICAgICAgICAg +ICJ0eXBlIjogInZvbHVtZSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAidm9s +dW1lIg0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAg +ICAgICAiZW5kcG9pbnRzIjogWw0KICAgICAgICAgICAgICAgICAgICB7DQogICAg +ICAgICAgICAgICAgICAgICAgICAiaWQiOiAiOTIyMTdhM2I5NTM5NDQ5Mjg1OWJj +NDlmZDQ3NDM4MmYiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFj +ZSI6ICJhZG1pbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0 +dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQogICAgICAgICAgICAgICAgICAgICAg +ICAicmVnaW9uIjogInJlZ2lvbk9uZSINCiAgICAgICAgICAgICAgICAgICAgfSwN +CiAgICAgICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAgICAg +ImlkIjogImYyMDU2M2JkZjY2ZjRlZmE4YTFmMTFkOTliNjcyYmUxIiwNCiAgICAg +ICAgICAgICAgICAgICAgICAgICJpbnRlcmZhY2UiOiAiaW50ZXJuYWwiLA0KICAg +ICAgICAgICAgICAgICAgICAgICAgInVybCI6ICJodHRwOi8vMTI3LjAuMC4xOjky +OTIvdjEiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6ICJyZWdp +b25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0sDQogICAgICAgICAgICAgICAg +ICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJpZCI6ICIzNzVmOWJhNDU5 +YTQ0NzczOGZiNjBmZTVmYzI2ZTlhYSIsDQogICAgICAgICAgICAgICAgICAgICAg +ICAiaW50ZXJmYWNlIjogInB1YmxpYyIsDQogICAgICAgICAgICAgICAgICAgICAg +ICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQogICAgICAgICAg +ICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSINCiAgICAgICAgICAg +ICAgICAgICAgfQ0KICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAg +ImlkIjogIjE1YzIxYWFlNmIyNzRhOGRhNTJlMGEwNjhlOTA4YWFjIiwNCiAgICAg +ICAgICAgICAgICAidHlwZSI6ICJpbWFnZSIsDQogICAgICAgICAgICAgICAgIm5h +bWUiOiAiZ2xhbmNlIg0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgIHsNCiAg +ICAgICAgICAgICAgICAiZW5kcG9pbnRzIjogWw0KICAgICAgICAgICAgICAgICAg +ICB7DQogICAgICAgICAgICAgICAgICAgICAgICAiaWQiOiAiZWRiZDlmNTBmNjY3 +NDZhZTllZDExZGMzYjFhZTM1ZGEiLA0KICAgICAgICAgICAgICAgICAgICAgICAg +ImludGVyZmFjZSI6ICJhZG1pbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAi +dXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQz +NWU4YTYwZmNmODliYjY2MTdhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJy +ZWdpb24iOiAicmVnaW9uT25lIg0KICAgICAgICAgICAgICAgICAgICB9LA0KICAg +ICAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICAgICAiaWQi +OiAiOWUwM2M0NmM4MGEzNGExNTljYjM5ZjVjYjA0OThiOTIiLA0KICAgICAgICAg +ICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJpbnRlcm5hbCIsDQogICAgICAg +ICAgICAgICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92 +MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwNCiAgICAgICAg +ICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25lIg0KICAgICAgICAg +ICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgICB7DQogICAgICAgICAg +ICAgICAgICAgICAgICAiaWQiOiAiMWRmMGI0NGQ5MjYzNGQ1OWJkMGUwZDYwY2Y3 +Y2U0MzIiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJw +dWJsaWMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInVybCI6ICJodHRwOi8v +MTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2 +NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lv +bk9uZSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgIF0s +DQogICAgICAgICAgICAgICAgImlkIjogIjJmNDA0ZmRiODkxNTRjNTg5ZWZiYzEw +NzI2YjAyOWVjIiwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJjb21wdXRlIiwN +CiAgICAgICAgICAgICAgICAibmFtZSI6ICJub3ZhIg0KICAgICAgICAgICAgfSwN +CiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzIjogWw0K +ICAgICAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICAgICAi +aWQiOiAiYTQ1MDFlMTQxYTRiNGUxNGJmMjgyZTdiZmZkODFkYzUiLA0KICAgICAg +ICAgICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJhZG1pbiIsDQogICAgICAg +ICAgICAgICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUzNTcv +djMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6ICJSZWdpb25P +bmUiDQogICAgICAgICAgICAgICAgICAgIH0sDQogICAgICAgICAgICAgICAgICAg +IHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJpZCI6ICIzZDE3ZTMyMjdiZmM0 +NDgzYjU4ZGU1ZWFhNTg0ZTM2MCIsDQogICAgICAgICAgICAgICAgICAgICAgICAi +aW50ZXJmYWNlIjogImludGVybmFsIiwNCiAgICAgICAgICAgICAgICAgICAgICAg +ICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTozNTM1Ny92MyIsDQogICAgICAgICAg +ICAgICAgICAgICAgICAicmVnaW9uIjogIlJlZ2lvbk9uZSINCiAgICAgICAgICAg +ICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAgICAgICAg +ICAgICAgICAgICAgImlkIjogIjhjZDRiOTU3MDkwZjRjYTU4NDJhMjJlOWE3NDA5 +OWNkIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRlcmZhY2UiOiAicHVi +bGljIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEy +Ny4wLjAuMTo1MDAwL3YzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdp +b24iOiAiUmVnaW9uT25lIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAg +ICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAiaWQiOiAiYzVkOTI2ZDU2NjQy +NGU0ZmJhNGY4MGMzNzkxNmNkZTUiLA0KICAgICAgICAgICAgICAgICJ0eXBlIjog +ImlkZW50aXR5IiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJrZXlzdG9uZSIN +CiAgICAgICAgICAgIH0NCiAgICAgICAgXSwNCiAgICAgICAgImlzc3VlZF9hdCI6 +ICIyMDAyLTAxLTE4VDIxOjE0OjA3WiIsDQogICAgICAgICJleHBpcmVzX2F0Ijog +IjIwMzgtMDEtMThUMjE6MTQ6MDdaIiwNCiAgICAgICAgImF1ZGl0X2lkcyI6IFsi +Wnp6WjJaWllxVDhPemZVVnZyakVJVFEiLCAiY0NDQ0NDY3RUek8xLVhVazVTVHli +dyJdLA0KICAgICAgICAicHJvamVjdCI6IHsNCiAgICAgICAgICAgICJlbmFibGVk +IjogdHJ1ZSwNCiAgICAgICAgICAgICJkZXNjcmlwdGlvbiI6IG51bGwsDQogICAg +ICAgICAgICAibmFtZSI6ICJ0ZW5hbnRfbmFtZTEiLA0KICAgICAgICAgICAgImlk +IjogInRlbmFudF9pZDEiLA0KICAgICAgICAgICAgImRvbWFpbiI6IHsNCiAgICAg +ICAgICAgICAgICAiaWQiOiAiZG9tYWluX2lkMSIsDQogICAgICAgICAgICAgICAg +Im5hbWUiOiAiZG9tYWluX25hbWUxIg0KICAgICAgICAgICAgfQ0KICAgICAgICB9 +LA0KICAgICAgICAidXNlciI6IHsNCiAgICAgICAgICAgICJuYW1lIjogInJldm9r +ZWRfdXNlcm5hbWUxIiwNCiAgICAgICAgICAgICJpZCI6ICJyZXZva2VkX3VzZXJf +aWQxIiwNCiAgICAgICAgICAgICJkb21haW4iOiB7DQogICAgICAgICAgICAgICAg +ImlkIjogImRvbWFpbl9pZDEiLA0KICAgICAgICAgICAgICAgICJuYW1lIjogImRv +bWFpbl9uYW1lMSINCiAgICAgICAgICAgIH0NCiAgICAgICAgfSwNCiAgICAgICAg +InJvbGVzIjogWw0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICJpZCI6 +ICJmMDNmZGE4ZjhhMzI0OWIyYTcwZmIxZjE3NmE3YjYzMSIsDQogICAgICAgICAg +ICAgICAgIm5hbWUiOiAicm9sZTEiDQogICAgICAgICAgICB9LA0KICAgICAgICAg +ICAgew0KICAgICAgICAgICAgICAgICJpZCI6ICJmMDNmZGE4ZjhhMzI0OWIyYTcw +ZmIxZjE3NmE3YjYzMSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAicm9sZTIi +DQogICAgICAgICAgICB9DQogICAgICAgIF0sDQogICAgICAgICJtZXRob2RzIjog +Ww0KICAgICAgICAgICAgInBhc3N3b3JkIg0KICAgICAgICBdDQogICAgfQ0KfQ0K +MYIBzjCCAcoCAQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYD +VQQIDAJDQTESMBAGA1UEBwwJU3Vubnl2YWxlMRIwEAYDVQQKDAlPcGVuU3RhY2sx +ETAPBgNVBAsMCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVu +c3RhY2sub3JnMRQwEgYDVQQDDAtTZWxmIFNpZ25lZAIBETALBglghkgBZQMEAgEw +DQYJKoZIhvcNAQEBBQAEggEAh4ESJgQ+2tuZrQdOUGXKi5uO56bbX5c15WdhPcHA +gXK4TUXC8Gb0pTyAGXXWkHwErf+7NHrZbA5Y8zqTor8qjig+tT8Ywh82lzY+mXOE +HgnkKNoGUmgv1auJuECjysLHX6T5c0AUOxPSBvCQtvmGl33/riX5/gM+D/Dkptul +iEOGXZc/S8qoOJE/SoJnFhimbJ7BuECKO8euTXeQrpKVyAULTm0RSQogWMTlXHzk +hY71+Qn0dpp4ZCQTKaFO2u6aYQ2fjJ3BlH8UK0edIUJ4cHqKfMm/TCiIHQ/jbEOp +cy83wzZMoDNK+7r/fxdUz7CJA1r2LrbWJMOhDVgKIYE6TA== +-----END CMS----- diff --git a/examples/pki/cms/auth_v3_token_revoked.pkiz b/examples/pki/cms/auth_v3_token_revoked.pkiz new file mode 100644 index 000000000..cf27adff0 --- /dev/null +++ b/examples/pki/cms/auth_v3_token_revoked.pkiz @@ -0,0 +1 @@ +PKIZ_eJydWEt3ozgT3etXfPucPgPCpOPFLHgZ47FEwDws7QxODELYTvzA8OunsJ3uPDrT6a9z-uSE4FLdqntvlfLtG_wzHdej_7PIrP_hGyKeR4qGTf7ZcK845tQIcmsDzx5sy7LHgWUEzsmKjLG5ip_tFbFcYVnWNnCt2ZM78zIN2YEzNhbwcBM7q9WT-dBOlAzvZVZ6t954V2ZpoizcYZW38PNoV-buqMu15TGvg3I-a1bIW0-OmZt0ntisUm1XLtKg9Euj5MLoeB0WvssGLCIttWVJakcjLi9Jyk604wXFHkakc8qpZZRZPdrzGZxiTdoMnySZTYZTy_zu1bLqg3s1awjmFYuK2nedjohAZ2JSINqZNROjmkQ5ZtGypIKcvLKBD-hFlsbnbPJ6uOORVz4myg4OkA9jc5vXSTfHIwWdowuvId1qT2xnT6IiJsK5JVFwS-zl4hxsbUJWW8m0Ht6rrNahRJD6YTkabrl9gcLd4Z6l8tC_AAXdcusMq8qwWixS_RFq9CZDdC7Y9UNzfH548taXVCF4CQU-n7YcT1Q-6z8YyhzTdjE3lUU6PJwhZOtkl1lvcS_d5MBSXXkXVLB5WGTucP3SNcRTvcrd4TZbh69aqSt8PqlZSuWlA6Mq62Gd65G02QXWZjkOG4BwdySRp02FcSDW4OSLlUY7dlwK50hFWNKaAR_g5C7uqE1UHhUFFdA5zAZ-OikRFUAKfCkgtGbd47pUeCI5lsesGh6AH33614J6HROJpFGssJrWiESOwoWn-DaVQJATq2ONRYZKbF69ItMBatLyeiSuZOshyxxqhgBPH13N6-ZcvMU4VHJ7c5x2TkvbQXOGFm0GtMvxVGOnaccUJngNrCwZJipQOehoGgPfWcMhJR84zwT8KloWl6JdoZQXmvN0uc2wfp_V8Rk2ehEPjcKC1UHLXaJQAV_upKAuiEdUJxoFei8qntKiJ9wj8KEnWQ8D5TUvGL5yfpwAcaT4Vbs-6xb6ars-6xb6j3aB9h2Np6B71zsxUSkkqrAPwSkGiDbAgQ5CG6Xk0K7eXUBdeu5eqQwSXqaqvAjnqj4Ra6BQAR0QELz1o0BDBCIRDF9dIf2UQh8czDpavPeEHwF7TYDVvUgA_b8aeCkq-lnVClLyeg38Ca11RITXVxf4XamkqxRqQyA71llNFD_lFbVzBdyq5eWvaY1e8_qLtNaBXG1P6x4a-h1Vf9q819CIdawOawpaoG5Sg0MXqPd4kgJVUw_TblJCb99Q9XdMRZ9T9WtFRe_NgnZJDdQtaF_IKFBAxp0P06inNY8g7c7DPJIFu5IPvWbfIlULjt9iJ1EiiBgVLI0xE54GCh1w11FJHTdgPBj5bqwTu4AXyPsRt87c0aHHf60J2HzYZBjaOCb9gMn6OqH3hWJpuF-kg3Ow5XyyuzCyUJZj43ba3p2IyPsaQUudW9_ONUStISazwQer-qpTod_2Pw1LbsuaRib0n2nU5iBjULDtnMC9OgSTGR6Agbif9_8qMphUzQdo6DNsX4WG_tSFX6D5aQwLB1EQrckA3KWD_oL7SsEE0blI4Luh-FFR-v1i8R_URn_mwkFP7QOZ3WHwSTA2gADzTdCIgOaTfrRhWKIEFyvwAxCXcDR2ofoVyuC68lx0EWFdoremOaq4MEtqhxWkDj4ZVgAL2mhK4gYQHDwTUwm233prdXmeTMuxbK7UFbDGNMt5-M6JJzW1DQVWvtK3-ykVQsYrHey-ZJ3TwJbmgUiM1k8T8d6Js3qI2Y8JnRz42Dz2nLgsnfuzvaF3Y7vgrrqFFn7F2jqonYpoC4RpPxYqflWoN5BqR6GRceqnEklhMjERShKFvecNSL9c1Lzm9qrnufoyRD7Oi4szg_R36Gsc6Ckca-DE3Xu29p44-4yuBAcwM2LYi70-MxiowYBgT_XdUNI0KVgUDxB1YZwL44-c-HWm6G2qIBDbALqSj04sfz3eAII3YDhQ-tFGO0MnLsgXO6pvBy2LvLZ3YNBA44PQuPVxDYAdKZSQ9nY5rt7gZ11ypjO3Y9BE0AJUYGO_NzFQLwH7B1bWtEI8it-78TOfy27p9qm-nJh0fO5dV_3wmKWj7cuV6MeW9nNjl7BgnjGChanXvl8_JIfnZ5cF9HIoernm8Dk_LnBSzbX-tMn1xddT68M757sDuq7xxTKFOvQXj-vQ8OT29kFu2lRueZ4Eg0jb1knCcV5vR7MkDC_0pjaC6WcHmCrJeHtPZipL0p0aq6HO1vn5Wge0hWteIvloWCwv87OFVrdT0MM0cgYosT3ov6P4wtBS2EIeI9cy8k2zWo1dY-WYxHMr-P9Agk1jGcxOgmDkNDAbg11jBcxG8MB1mkkSd86UGJVrqLFjmcQKFOfkCCMwVzQxjTyyEqpmta4vQUCgxBkxjfO7yCrIJNJMmUmqgNqeSeg0dnM-aeo0xfRHSyNHEov8uPLCjXdihCxFUFU916BNdWJkfaD1JdC0Hra8c2JieueTjBOZxjjZ8dKMFunywFO4V8NhyGzY6J9mYBvFprGD15dwxzQDA-7TjtGOxMIPR9FjE37XlON2OLSOB7sjD972Dj3vk-d2KBcPs-i21Uf3T1YNbT7l2DUf13g_cx-GOztaP5Uj9n3HlcmoCOybMtQmpSKD5BQhO1wbnuf8VQxq81BFdydp7pVVfCrNm25YBtSST48zfdt8V7ARb2Ot3DaMTjl_rr2tk_ylofomW_jOatpsRxv_xr9ZVVYs75RsIBUdVHqzte-8gzmYPVZW87zOHruTtDb5Kdb8h0X2qHkomd3WT8Pd6GCnj3KCT-U8NZXn440q0yM7TXLn4K6G08aLk0qtd_e3M3pj4XEi56UbYWMNV1823R3Nm6ySHdnj1nkw_MhV8NPqefKPqdLh-AFnnXW_N-82BSmq2VgeqvvWHAyCv_9G5z-CONT--QeRfwEmaLr4 \ No newline at end of file diff --git a/examples/pki/cms/auth_v3_token_scoped.json b/examples/pki/cms/auth_v3_token_scoped.json new file mode 100644 index 000000000..0fd000a0d --- /dev/null +++ b/examples/pki/cms/auth_v3_token_scoped.json @@ -0,0 +1,140 @@ +{ + "token": { + "methods": [ + "password" + ], + "roles": [ + { + "id": "f03fda8f8a3249b2a70fb1f176a7b631", + "name": "role1" + }, + { + "id": "f03fda8f8a3249b2a70fb1f176a7b631", + "name": "role2" + } + ], + "issued_at": "2002-01-18T21:14:07Z", + "expires_at": "2038-01-18T21:14:07Z", + "audit_ids": ["VcxU2JYqT8OzfUVvrjEITQ", "qNUTIJntTzO1-XUk5STybw"], + "project": { + "id": "tenant_id1", + "domain": { + "id": "domain_id1", + "name": "domain_name1" + }, + "enabled": true, + "description": null, + "name": "tenant_name1" + }, + "catalog": [ + { + "endpoints": [ + { + "id": "3b5e554bcf114f2483e8a1be7a0506d1", + "interface": "admin", + "url": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + }, + { + "id": "54abd2dc463c4ba4a72915498f8ecad1", + "interface": "internal", + "url": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + }, + { + "id": "70a7efa4b1b941968357cc43ae1419ee", + "interface": "public", + "url": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + } + ], + "id": "5707c3fc0a294703a3c638e9cf6a6c3a", + "type": "volume", + "name": "volume" + }, + { + "endpoints": [ + { + "id": "92217a3b95394492859bc49fd474382f", + "interface": "admin", + "url": "http://127.0.0.1:9292/v1", + "region": "regionOne" + }, + { + "id": "f20563bdf66f4efa8a1f11d99b672be1", + "interface": "internal", + "url": "http://127.0.0.1:9292/v1", + "region": "regionOne" + }, + { + "id": "375f9ba459a447738fb60fe5fc26e9aa", + "interface": "public", + "url": "http://127.0.0.1:9292/v1", + "region": "regionOne" + } + ], + "id": "15c21aae6b274a8da52e0a068e908aac", + "type": "image", + "name": "glance" + }, + { + "endpoints": [ + { + "id": "edbd9f50f66746ae9ed11dc3b1ae35da", + "interface": "admin", + "url": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + }, + { + "id": "9e03c46c80a34a159cb39f5cb0498b92", + "interface": "internal", + "url": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + }, + { + "id": "1df0b44d92634d59bd0e0d60cf7ce432", + "interface": "public", + "url": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", + "region": "regionOne" + } + ], + "id": "2f404fdb89154c589efbc10726b029ec", + "type": "compute", + "name": "nova" + }, + { + "endpoints": [ + { + "id": "a4501e141a4b4e14bf282e7bffd81dc5", + "interface": "admin", + "url": "http://127.0.0.1:35357/v3", + "region": "RegionOne" + }, + { + "id": "3d17e3227bfc4483b58de5eaa584e360", + "interface": "internal", + "url": "http://127.0.0.1:35357/v3", + "region": "RegionOne" + }, + { + "id": "8cd4b957090f4ca5842a22e9a74099cd", + "interface": "public", + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne" + } + ], + "id": "c5d926d566424e4fba4f80c37916cde5", + "type": "identity", + "name": "keystone" + } + ], + "user": { + "domain": { + "id": "domain_id1", + "name": "domain_name1" + }, + "name": "user_name1", + "id": "user_id1" + } + } +} diff --git a/examples/pki/cms/auth_v3_token_scoped.pem b/examples/pki/cms/auth_v3_token_scoped.pem new file mode 100644 index 000000000..ae3f4f37c --- /dev/null +++ b/examples/pki/cms/auth_v3_token_scoped.pem @@ -0,0 +1,125 @@ +-----BEGIN CMS----- +MIIW7gYJKoZIhvcNAQcCoIIW3zCCFtsCAQExDTALBglghkgBZQMEAgEwghTzBgkq +hkiG9w0BBwGgghTkBIIU4HsNCiAgICAidG9rZW4iOiB7DQogICAgICAgICJtZXRo +b2RzIjogWw0KICAgICAgICAgICAgInBhc3N3b3JkIg0KICAgICAgICBdLA0KICAg +ICAgICAicm9sZXMiOiBbDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAg +ImlkIjogImYwM2ZkYThmOGEzMjQ5YjJhNzBmYjFmMTc2YTdiNjMxIiwNCiAgICAg +ICAgICAgICAgICAibmFtZSI6ICJyb2xlMSINCiAgICAgICAgICAgIH0sDQogICAg +ICAgICAgICB7DQogICAgICAgICAgICAgICAgImlkIjogImYwM2ZkYThmOGEzMjQ5 +YjJhNzBmYjFmMTc2YTdiNjMxIiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJy +b2xlMiINCiAgICAgICAgICAgIH0NCiAgICAgICAgXSwNCiAgICAgICAgImlzc3Vl +ZF9hdCI6ICIyMDAyLTAxLTE4VDIxOjE0OjA3WiIsDQogICAgICAgICJleHBpcmVz +X2F0IjogIjIwMzgtMDEtMThUMjE6MTQ6MDdaIiwNCiAgICAgICAgImF1ZGl0X2lk +cyI6IFsiVmN4VTJKWXFUOE96ZlVWdnJqRUlUUSIsICJxTlVUSUpudFR6TzEtWFVr +NVNUeWJ3Il0sDQogICAgICAgICJwcm9qZWN0Ijogew0KICAgICAgICAgICAgImlk +IjogInRlbmFudF9pZDEiLA0KICAgICAgICAgICAgImRvbWFpbiI6IHsNCiAgICAg +ICAgICAgICAgICAiaWQiOiAiZG9tYWluX2lkMSIsDQogICAgICAgICAgICAgICAg +Im5hbWUiOiAiZG9tYWluX25hbWUxIg0KICAgICAgICAgICAgfSwNCiAgICAgICAg +ICAgICJlbmFibGVkIjogdHJ1ZSwNCiAgICAgICAgICAgICJkZXNjcmlwdGlvbiI6 +IG51bGwsDQogICAgICAgICAgICAibmFtZSI6ICJ0ZW5hbnRfbmFtZTEiDQogICAg +ICAgIH0sDQogICAgICAgICJjYXRhbG9nIjogWw0KICAgICAgICAgICAgew0KICAg +ICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAg +IHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJpZCI6ICIzYjVlNTU0YmNmMTE0 +ZjI0ODNlOGExYmU3YTA1MDZkMSIsDQogICAgICAgICAgICAgICAgICAgICAgICAi +aW50ZXJmYWNlIjogImFkbWluIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ1 +cmwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4 +YTYwZmNmODliYjY2MTdhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdp +b24iOiAicmVnaW9uT25lIg0KICAgICAgICAgICAgICAgICAgICB9LA0KICAgICAg +ICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICAgICAiaWQiOiAi +NTRhYmQyZGM0NjNjNGJhNGE3MjkxNTQ5OGY4ZWNhZDEiLA0KICAgICAgICAgICAg +ICAgICAgICAgICAgImludGVyZmFjZSI6ICJpbnRlcm5hbCIsDQogICAgICAgICAg +ICAgICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3Ni92MS82 +NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIsDQogICAgICAgICAgICAg +ICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSINCiAgICAgICAgICAgICAg +ICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAg +ICAgICAgICAgImlkIjogIjcwYTdlZmE0YjFiOTQxOTY4MzU3Y2M0M2FlMTQxOWVl +IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRlcmZhY2UiOiAicHVibGlj +IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4w +LjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwN +CiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25lIg0K +ICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAgICAg +ICAgICAgICAgICAiaWQiOiAiNTcwN2MzZmMwYTI5NDcwM2EzYzYzOGU5Y2Y2YTZj +M2EiLA0KICAgICAgICAgICAgICAgICJ0eXBlIjogInZvbHVtZSIsDQogICAgICAg +ICAgICAgICAgIm5hbWUiOiAidm9sdW1lIg0KICAgICAgICAgICAgfSwNCiAgICAg +ICAgICAgIHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzIjogWw0KICAgICAg +ICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICAgICAiaWQiOiAi +OTIyMTdhM2I5NTM5NDQ5Mjg1OWJjNDlmZDQ3NDM4MmYiLA0KICAgICAgICAgICAg +ICAgICAgICAgICAgImludGVyZmFjZSI6ICJhZG1pbiIsDQogICAgICAgICAgICAg +ICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5Mi92MSIsDQog +ICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSINCiAg +ICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICAgew0KICAg +ICAgICAgICAgICAgICAgICAgICAgImlkIjogImYyMDU2M2JkZjY2ZjRlZmE4YTFm +MTFkOTliNjcyYmUxIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRlcmZh +Y2UiOiAiaW50ZXJuYWwiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInVybCI6 +ICJodHRwOi8vMTI3LjAuMC4xOjkyOTIvdjEiLA0KICAgICAgICAgICAgICAgICAg +ICAgICAgInJlZ2lvbiI6ICJyZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAg +IH0sDQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAg +ICAgICJpZCI6ICIzNzVmOWJhNDU5YTQ0NzczOGZiNjBmZTVmYzI2ZTlhYSIsDQog +ICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJmYWNlIjogInB1YmxpYyIsDQog +ICAgICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6 +OTI5Mi92MSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJl +Z2lvbk9uZSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAg +IF0sDQogICAgICAgICAgICAgICAgImlkIjogIjE1YzIxYWFlNmIyNzRhOGRhNTJl +MGEwNjhlOTA4YWFjIiwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJpbWFnZSIs +DQogICAgICAgICAgICAgICAgIm5hbWUiOiAiZ2xhbmNlIg0KICAgICAgICAgICAg +fSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzIjog +Ww0KICAgICAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICAg +ICAiaWQiOiAiZWRiZDlmNTBmNjY3NDZhZTllZDExZGMzYjFhZTM1ZGEiLA0KICAg +ICAgICAgICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJhZG1pbiIsDQogICAg +ICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3 +NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwNCiAgICAg +ICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25lIg0KICAgICAg +ICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgICB7DQogICAgICAg +ICAgICAgICAgICAgICAgICAiaWQiOiAiOWUwM2M0NmM4MGEzNGExNTljYjM5ZjVj +YjA0OThiOTIiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFjZSI6 +ICJpbnRlcm5hbCIsDQogICAgICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0 +dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNm +ODliYjY2MTdhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAi +cmVnaW9uT25lIg0KICAgICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAg +ICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICAgICAiaWQiOiAiMWRmMGI0 +NGQ5MjYzNGQ1OWJkMGUwZDYwY2Y3Y2U0MzIiLA0KICAgICAgICAgICAgICAgICAg +ICAgICAgImludGVyZmFjZSI6ICJwdWJsaWMiLA0KICAgICAgICAgICAgICAgICAg +ICAgICAgInVybCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNm +YmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAg +ICAgICAicmVnaW9uIjogInJlZ2lvbk9uZSINCiAgICAgICAgICAgICAgICAgICAg +fQ0KICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgImlkIjogIjJm +NDA0ZmRiODkxNTRjNTg5ZWZiYzEwNzI2YjAyOWVjIiwNCiAgICAgICAgICAgICAg +ICAidHlwZSI6ICJjb21wdXRlIiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJu +b3ZhIg0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAg +ICAgICAiZW5kcG9pbnRzIjogWw0KICAgICAgICAgICAgICAgICAgICB7DQogICAg +ICAgICAgICAgICAgICAgICAgICAiaWQiOiAiYTQ1MDFlMTQxYTRiNGUxNGJmMjgy +ZTdiZmZkODFkYzUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFj +ZSI6ICJhZG1pbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0 +dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjMiLA0KICAgICAgICAgICAgICAgICAgICAg +ICAgInJlZ2lvbiI6ICJSZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0s +DQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAg +ICJpZCI6ICIzZDE3ZTMyMjdiZmM0NDgzYjU4ZGU1ZWFhNTg0ZTM2MCIsDQogICAg +ICAgICAgICAgICAgICAgICAgICAiaW50ZXJmYWNlIjogImludGVybmFsIiwNCiAg +ICAgICAgICAgICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMToz +NTM1Ny92MyIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogIlJl +Z2lvbk9uZSINCiAgICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAg +ICAgICAgew0KICAgICAgICAgICAgICAgICAgICAgICAgImlkIjogIjhjZDRiOTU3 +MDkwZjRjYTU4NDJhMjJlOWE3NDA5OWNkIiwNCiAgICAgICAgICAgICAgICAgICAg +ICAgICJpbnRlcmZhY2UiOiAicHVibGljIiwNCiAgICAgICAgICAgICAgICAgICAg +ICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTo1MDAwL3YzIiwNCiAgICAgICAg +ICAgICAgICAgICAgICAgICJyZWdpb24iOiAiUmVnaW9uT25lIg0KICAgICAgICAg +ICAgICAgICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAg +ICAiaWQiOiAiYzVkOTI2ZDU2NjQyNGU0ZmJhNGY4MGMzNzkxNmNkZTUiLA0KICAg +ICAgICAgICAgICAgICJ0eXBlIjogImlkZW50aXR5IiwNCiAgICAgICAgICAgICAg +ICAibmFtZSI6ICJrZXlzdG9uZSINCiAgICAgICAgICAgIH0NCiAgICAgICAgXSwN +CiAgICAgICAgInVzZXIiOiB7DQogICAgICAgICAgICAiZG9tYWluIjogew0KICAg +ICAgICAgICAgICAgICJpZCI6ICJkb21haW5faWQxIiwNCiAgICAgICAgICAgICAg +ICAibmFtZSI6ICJkb21haW5fbmFtZTEiDQogICAgICAgICAgICB9LA0KICAgICAg +ICAgICAgIm5hbWUiOiAidXNlcl9uYW1lMSIsDQogICAgICAgICAgICAiaWQiOiAi +dXNlcl9pZDEiDQogICAgICAgIH0NCiAgICB9DQp9DQoxggHOMIIBygIBATCBpDCB +njEKMAgGA1UEBRMBNTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQH +DAlTdW5ueXZhbGUxEjAQBgNVBAoMCU9wZW5TdGFjazERMA8GA1UECwwIS2V5c3Rv +bmUxJTAjBgkqhkiG9w0BCQEWFmtleXN0b25lQG9wZW5zdGFjay5vcmcxFDASBgNV +BAMMC1NlbGYgU2lnbmVkAgERMAsGCWCGSAFlAwQCATANBgkqhkiG9w0BAQEFAASC +AQAKyFuKuktH9YR3TfKG4NAEBXDkEAkWA+fVWREyEnYoCYE2YDiqrVRwpFqR6p0V +PGRUPcDLTtPw+6p+ywNbP3F3AU0LNqjs3zl8HxvHij91CzZOQIykUcX4ToLJhBAD +shZzSucjk5y9zTLN4bl+AX/NJ/GYT2chFVjYJUW+sbIVoT5u0V9K602OV9OAyvpY +a0YzFmnEzplkg2U0qzZjx5b143ASQnaCtaf4rK7HCOIz11I6aaL3yhGSxnqE6yyA +9FdYOwB14E1GWeLVx0xeM8D/qMesqANsk0WQ19LnvKB7ry5EnDCGeBRYTMR+u0B2 +gnCYSRs9m+gg1rL889d53c/q +-----END CMS----- diff --git a/examples/pki/cms/auth_v3_token_scoped.pkiz b/examples/pki/cms/auth_v3_token_scoped.pkiz new file mode 100644 index 000000000..3365dfe51 --- /dev/null +++ b/examples/pki/cms/auth_v3_token_scoped.pkiz @@ -0,0 +1 @@ +PKIZ_eJylWEl34rwS3etXvH2fPu0BElh6wthBcmw8IO2wnWDLNiRh8PDrXwlId4bu70u_lyxycKBUt-reWyW-f4cf3bId8h8DL8WL7wg7Ds5b6t7tmFOcMqL5mbGDZ2vTMEzbNzTf6oxQm-ub6MXcYMPmhmHsfNtYPttLJ1WR6VtzbQ0Pt5G12Tx1D70rpcqhTkvnxpnvyzSJpbU9rbIeXs_2ZWbPhkzNT1njl6tlu0HO1j2ldjw4fLdJ1H25TvzSK7WScW1gTVB4Nh3REPfErEvcWCq2WYkT2pGBFURxFIQHq1wYWpk2swNbwimG26dKV-OlO10Y-q3T1JUI7jS0xQqraFg0nm0NmPtjyt0CkUFvKJ81OMwUGuYl4bhzyhY-MC7SJDpnkzXTPQud8jGW9nBA_TDXn7ImHlbKTELn6Nxp8bA5YNM64LCIMLducOjfYDNfn4NtdcjqqaaqgCeyCk5pMnsSdUKiUD9x29MDTerjSqkrvHTEaUeayPUFwvVD9fT87AJRKxFLxgVtupoZoupBnyeR-ODT-bXhSuL_6TZ4hEM-Qcvt-IhoMpZWyvnh9Q1BnSmkX690aZ1Mj-L0dBvv0_kZP6eroEjt6fa1ayKDKrOnT3DKm1aOJbZyG5qQa_qzKgVol3rEfXrJbpfPgxZ55eSEQ0ddcO2IjVHn8Y1KBnrKuXUiPChJQ4EPcPIQDcTEMguLgnDonEJHXuKWiHAghXLhArRm-5o2EKxmSn1Kq-mRXQp6rYszUB7XJIwk2pAG4dCSGHckzyQ1EKSjTaTSUJOxyao3ZDpCwXrWzPiVbAJynUFBEeAR0eWsac-VXc8DKTN3p8Vg9aQftWdo4W5EhkxZqLRbDFSinDXAypIqWAYq-wNJIuA7bRmk5AHnKYd_hXlxKdoVSnmhOUvyp1QZ36dNdIaNXklEwgD44PfMxhLh8Gu7BbFBPLzqSOiPhahYQgpmWuUjqBBUe4aBsoYVVLlyfh6XqV3z37XrT91CX23Xn7qF_qFdoH1LZQno3nY6yisJh5XiQXCiAEQT4EAHoY11zaBdwl2cbTDO7CvPQcK5ENKZ3ldP4JEKCuXQAQ7Bey_0VYQhElbgdyhqLyHQB0uhAyk-Cec14BY0AQp-lQD6XzXwWlT0q6oVpOQIDfwNrccIc0dUF_hdyXioJGJCIDMa0wZLXsIqYmYSuFXPyt_TGr3l9RdpPQZy9YLWAhr6N6r-snmnJSEdaBM0BLRA7LgBhy6Q8HicAFUTRyGDW0Jv31H135iK_kzVrxUVfTQLMsQNULcgopChL4GMBw-mkaA1CyHtwVFYWBf0Sj70ln3rRC6Y8h47DmOO-aygSaRQ7qig0BGzLRk3UQvGoyDPjsbYLOAN-OOI26b27CjwX2tSp03Qpgq0cY7FgElFndDHQtEkOKyT0TlYvnL3F0YWUj7Xbhb9pMM8EzWCllo3npmpiBhTBS9Hn6zqq06F_rX_SVAys25IqEP_qUpMBjIGBZtWB-41IJjM8AAMxP5z_68ig5nYfoKG_oTtq9DQ37rwKzQviWDhwBIiDR6BuwzQX3DfmlOOx4zH8FeTvLAoPbFY_AO10d-5sC-ofcTLiQI-CcYGEGC-cRJi0HwsRpsCSxRnfAN-AOLilkovVL9CGV1XnosuQmVco_emOasY10tiBhWkDj4ZVAAL2qjX2PYhOHimQmqw_d7Zyvl5MuXzur1Sl6eK3Oar4IMTuw0xNQlWvtIzxZQKIOPNGOy-pIPVwpbmgEi03kti_tGJ02aq0J8TOj6yuX4SnLgsnYezvaEPY7tgtiy2r69Y2wC1kxHpgTD950JFbwr1DlJjSSTUOjGVcAKTifKgxmEgPG-ExXLRsIaZG8Fz-XWIfJ4XF2cG6e_R1zggKByp4MTDR7YKT1z-ia5Y8WFmRLAXOyIzGKj-CCuO7NlBTZK4oGE0QsSGcc61v3Lit5mi96mCQEwN6Io_O3H9-_EGEJwRVXxJjDYyaGNsg3wVS_ZMv6eh0wsHBg20HgiNGZ_XANiRghrSfsrn1Tv8dIjPdGZmBJrwe4AKbBR7EwX1YrB_YGVDKsTC6KMbv7BVPeS2SPX1xHhgK-fTqi9ajP6fVV8ciq6nypkS9--39ivzzqe7l3V_e97YizwByLPpE4P5gMSAcGrGH2ZRv6zrLjaL-4eGxfGW9esqduPZdTZG4zi26juoV_RQTbpFXMTrIQ5RPK_LvHfP2l6vyJAncSXuQj-vQqbz-6vQVp5i6uioh4ukllFxwWw3a7_dsFFncM3RNyTWtSjUwqgzBs29vIZpWMch9vet4VMz9n0HWa1r-qG1xLpma3Jk6R12IzU-pttaoQlc_wKntbTzm--str7P4JoTqbAWK_vOCrV7dIm8Dw3rUD-sCNxax1DlqHXeXYcrHcbPr_ZG-kkEyiAQgkjHVHW3OPBba3M-ybTaQ8iSrnFm5IlBQAaIrHf3Z43om-q5qEobTVtJB_wzTVtCHfQyJe6ErBKVzTaj52ktJzfLb5v18ZmC1e32cXCI5qRZzslkMg823voBTYYq2m8GfXVblmo0dM3LjN3xqVkYCzr6sV1KyTqii1l6WDem8cKauebfL3da5hH1YX0kB3S3s8JH95Yrw_PIcQ-yjZenh4leSlL5GLTsx2EXLshhkdaPVjtbG_2tGo-Ml930B6tGP4y9h0aBP1TLYtZNb8udfW9NW0t2T1M6dLvxMdzcDMroZlOOlIexdFw6M4Ybgp-rKB-k7Y2fHb4hecCPk-Tbg_zUV3f7LNaH2_HtbLr99qwVnTxedF1u3DkvvgwjZpJr_SLQ_Uh6Zg-LZnFaqgnaNo8_3Nq_XZh-86yufGsepL68H-fDj6bFE6dcNhN0_rLDIuavLz7-Cz0ItdI= \ No newline at end of file diff --git a/doc/source/_templates/.placeholder b/examples/pki/cms/revocation_list.der similarity index 100% rename from doc/source/_templates/.placeholder rename to examples/pki/cms/revocation_list.der diff --git a/examples/pki/cms/revocation_list.json b/examples/pki/cms/revocation_list.json index c3401b0fb..766c69c94 100644 --- a/examples/pki/cms/revocation_list.json +++ b/examples/pki/cms/revocation_list.json @@ -1 +1 @@ -{"revoked":[{"id":"7acfcfdaf6a14aebe97c61c5947bc4d3","expires":"2012-08-14T17:58:48Z"}]} +{"revoked": [{"expires": "2112-08-14T17:58:48Z", "id": "db98ed2af6c6707bec6dc6c6892789a0"}, {"expires": "2112-08-14T17:58:48Z", "id": "15ce05fd491b79791068ed80a9c7f5e7"}, {"expires": "2112-08-14T17:58:48Z", "id": "db98ed2af6c6707bec6dc6c6892789a0"}, {"expires": "2112-08-14T17:58:48Z", "id": "15ce05fd491b79791068ed80a9c7f5e7"}]} \ No newline at end of file diff --git a/examples/pki/cms/revocation_list.pem b/examples/pki/cms/revocation_list.pem index 969d10dd3..e37f9de4a 100644 --- a/examples/pki/cms/revocation_list.pem +++ b/examples/pki/cms/revocation_list.pem @@ -1,12 +1,20 @@ -----BEGIN CMS----- -MIIB2QYJKoZIhvcNAQcCoIIByjCCAcYCAQExCTAHBgUrDgMCGjBpBgkqhkiG9w0B -BwGgXARaeyJyZXZva2VkIjpbeyJpZCI6IjdhY2ZjZmRhZjZhMTRhZWJlOTdjNjFj -NTk0N2JjNGQzIiwiZXhwaXJlcyI6IjIwMTItMDgtMTRUMTc6NTg6NDhaIn1dfQ0K -MYIBSTCCAUUCAQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYD -VQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sx -ETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVu -c3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZAIBETAHBgUrDgMCGjANBgkq -hkiG9w0BAQEFAASBgDNDhvViAo8EqTVVvZ00pWUWjajTwoV1w1os1XDJ1XacBUo+ -rsh7gljIIVuvHL2F9C660I5jxhb7QVsTge3CwSiDmexxBAPOs4lNR5hFH7FdT47b -OK2qd0XnRjo5F7odUxIkozuQ/UISaNTPeWxGEMNVhpTXo2Dwn8wN1wrs/Z2E +MIIDVwYJKoZIhvcNAQcCoIIDSDCCA0QCAQExDTALBglghkgBZQMEAgEwggFcBgkq +hkiG9w0BBwGgggFNBIIBSXsicmV2b2tlZCI6IFt7ImV4cGlyZXMiOiAiMjExMi0w +OC0xNFQxNzo1ODo0OFoiLCAiaWQiOiAiZGI5OGVkMmFmNmM2NzA3YmVjNmRjNmM2 +ODkyNzg5YTAifSwgeyJleHBpcmVzIjogIjIxMTItMDgtMTRUMTc6NTg6NDhaIiwg +ImlkIjogIjE1Y2UwNWZkNDkxYjc5NzkxMDY4ZWQ4MGE5YzdmNWU3In0sIHsiZXhw +aXJlcyI6ICIyMTEyLTA4LTE0VDE3OjU4OjQ4WiIsICJpZCI6ICJkYjk4ZWQyYWY2 +YzY3MDdiZWM2ZGM2YzY4OTI3ODlhMCJ9LCB7ImV4cGlyZXMiOiAiMjExMi0wOC0x +NFQxNzo1ODo0OFoiLCAiaWQiOiAiMTVjZTA1ZmQ0OTFiNzk3OTEwNjhlZDgwYTlj +N2Y1ZTcifV19MYIBzjCCAcoCAQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYT +AlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU3Vubnl2YWxlMRIwEAYDVQQKDAlP +cGVuU3RhY2sxETAPBgNVBAsMCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlz +dG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDDAtTZWxmIFNpZ25lZAIBETALBglg +hkgBZQMEAgEwDQYJKoZIhvcNAQEBBQAEggEAVsD6Zy+bXz9D9ZJ32CbG4ggTisgs +4vZw9xemVMQ9J8+3iLWE2Z+NrqO1T4Q81iu9cKZ/zTFuu99B5X9ZHE57QLrOd+Jb +Ld3E8HYVj8ZpGQfXAcq3ybIkT2SeEddQmW+MlHHkkfu41D1dQ8jgkIcbLjpeRCFk +GA5Zj6Xdnozos/qMLqd1o4ufJ+dQAeFH/fviLkS5fiTPK6GmhqkwBQgffkOHTo/S +xZQeq7GhFKijg5p60AZRc1Q+WlrmkY9W0FrX7bt6iJodWBc2YSEZ5Mr7BpvIu5JH +rg282z/D4Xwj+USzn7jvqZFlmkG9o6s6YKjsrIpnPFH92EWv0+D5NuOuOA== -----END CMS----- diff --git a/examples/pki/cms/revocation_list.pkiz b/examples/pki/cms/revocation_list.pkiz new file mode 100644 index 000000000..566d33180 --- /dev/null +++ b/examples/pki/cms/revocation_list.pkiz @@ -0,0 +1 @@ +PKIZ_eJx9VElzszgUvPMr5p5KBbPY5vAdtGAsYokAYr0ZbIvdSXDM8usHO1Uzl6lRlQ6v1dVPr6vrvb4uB5oWYX8h6j-KV4kSgvmQ2O_XlBT3nAE3R9cFczFCYB4QcM0RcbCHIvjGgiKrWvBwsJD_ZfkkUyXsmntwXMBANoXY2efJntI4vR-VsCbVVURqX6ZxMRxju8knsiaITJSb04ED7cBNWQqxqTpVoDmVq0Ul6QmyP1P0INp1UtVaGrlTEiVKMicqxacyjaiSWvRRaw4nquTgpqDINg4IbkgbarnVLD-gpVOCklbmSEt5cJA8sp07svm6cvBVdnbX8oBAeYzcUnoSeVilHKzS1pUdvivZXKsONwdWFU2KxZDwpmJKskp5Xl78QSxjNuc9_MzbcJYec5KKjJSTG8XiRrkXUJ6vGRdrhosjKQdB2ubpB2m90uEPUbtIq7RiVT5ITLGbZE7r5S6A0GmVa05kDqSTe7L_fwMf_kn_bSAZWcQaisM2xa5OI7KMlOuUA8WxwtrBsHAiqqZV2Ehsso04lkch9u9LJuAoCAQcwU-MYFeZ7xQIC6wCE3oUMm4eKKh_68X6MKSjhGZgQ8FCCAQHNYPUI4MJEhy67t4cGn6K9J9znBaZFYxmBdxf7pWjwBjSSOfSydpVx9n0KNg-ldFIia-Eeq5696wNRpuDCor6q6hLyxhkiFwz2rW35hwzOVP0RnKtp9L8FJr0e97m4w4D_7cT5WjFmsxKRKA0XdaGNRCPZrkF_d4BAzlKFMj_5HqJNQRuAODiBbA6rf6eRvvnxNOEXlRFvHdXtj-D2MuMDbqiuOSiVyTx85Y18dtloKfvw9Y6COXzJ_HkTQxFddXXd9pjQ0uJNxW5v2p2t9K4n939bRN_buvM2zZSl43GpXcKOgb7g9eN5bU8A4Ovpvpjm96TUC0SdI5rkhQfAmsNAKBlX4aRjtDz1bw3JfzxEs-rlyC587XbvrHI-6k20ZK7S3cmcCP4-qCX330gf90ocs9fRD31H4bl95O2t-_QkyAks7u5mNRDFgc4q7W2eVnj8cVudd_ZyuxedvOIKkdd3nLTWn26qmeFZqeKaRbKUer7oxdoU54lAAHDeNeDGd38aisaK94dV3k3akrnd8ohu9gfK_pHeu4hk-F_d9Lf2fB_ww== \ No newline at end of file diff --git a/examples/pki/gen_cmsz.py b/examples/pki/gen_cmsz.py new file mode 100644 index 000000000..35a6a8f8c --- /dev/null +++ b/examples/pki/gen_cmsz.py @@ -0,0 +1,119 @@ +#!/usr/bin/python + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +from oslo_serialization import jsonutils + +from keystoneclient.common import cms +from keystoneclient import utils + +CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) + + +def make_filename(*args): + return os.path.join(CURRENT_DIR, *args) + + +def generate_revocation_list(): + REVOKED_TOKENS = ['auth_token_revoked', 'auth_v3_token_revoked'] + revoked_list = [] + for token in REVOKED_TOKENS: + with open(make_filename('cms', '%s.pkiz' % name), 'r') as f: + token_data = f.read() + id = utils.hash_signed_token(token_data.encode('utf-8')) + revoked_list.append({ + 'id': id, + "expires": "2112-08-14T17:58:48Z" + }) + with open(make_filename('cms', '%s.pem' % name), 'r') as f: + pem_data = f.read() + token_data = cms.cms_to_token(pem_data).encode('utf-8') + id = utils.hash_signed_token(token_data) + revoked_list.append({ + 'id': id, + "expires": "2112-08-14T17:58:48Z" + }) + revoked_json = jsonutils.dumps({"revoked": revoked_list}) + with open(make_filename('cms', 'revocation_list.json'), 'w') as f: + f.write(revoked_json) + encoded = cms.pkiz_sign(revoked_json, + SIGNING_CERT_FILE_NAME, + SIGNING_KEY_FILE_NAME) + with open(make_filename('cms', 'revocation_list.pkiz'), 'w') as f: + f.write(encoded) + + encoded = cms.cms_sign_data(revoked_json, + SIGNING_CERT_FILE_NAME, + SIGNING_KEY_FILE_NAME) + with open(make_filename('cms', 'revocation_list.pem'), 'w') as f: + f.write(encoded) + + +CA_CERT_FILE_NAME = make_filename('certs', 'cacert.pem') +SIGNING_CERT_FILE_NAME = make_filename('certs', 'signing_cert.pem') +SIGNING_KEY_FILE_NAME = make_filename('private', 'signing_key.pem') +EXAMPLE_TOKENS = ['auth_token_revoked', + 'auth_token_unscoped', + 'auth_token_scoped', + 'auth_token_scoped_expired', + 'auth_v3_token_scoped', + 'auth_v3_token_revoked'] + + +# Helper script to generate the sample data for testing +# the signed tokens using the existing JSON data for the +# MII-prefixed tokens. Uses the keys and certificates +# generated in gen_pki.sh. +def generate_der_form(name): + derfile = make_filename('cms', '%s.der' % name) + with open(derfile, 'w') as f: + derform = cms.cms_sign_data(text, + SIGNING_CERT_FILE_NAME, + SIGNING_KEY_FILE_NAME, cms.PKIZ_CMS_FORM) + f.write(derform) + + +for name in EXAMPLE_TOKENS: + json_file = make_filename('cms', name + '.json') + pkiz_file = make_filename('cms', name + '.pkiz') + with open(json_file, 'r') as f: + string_data = f.read() + + # validate the JSON + try: + token_data = jsonutils.loads(string_data) + except ValueError as v: + raise SystemExit('%s while processing token data from %s: %s' % + (v, json_file, string_data)) + + text = jsonutils.dumps(token_data).encode('utf-8') + + # Uncomment to record the token uncompressed, + # useful for debugging + # generate_der_form(name) + + encoded = cms.pkiz_sign(text, + SIGNING_CERT_FILE_NAME, + SIGNING_KEY_FILE_NAME) + + # verify before writing + cms.pkiz_verify(encoded, + SIGNING_CERT_FILE_NAME, + CA_CERT_FILE_NAME) + + with open(pkiz_file, 'w') as f: + f.write(encoded) + + generate_revocation_list() diff --git a/examples/pki/gen_pki.sh b/examples/pki/gen_pki.sh index 9bf6c3225..f3a3fddb0 100755 --- a/examples/pki/gen_pki.sh +++ b/examples/pki/gen_pki.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2012 OpenStack LLC +# Copyright 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -14,7 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. -# This script generates the crypto necessary for the SSL tests. +# These functions generate the certificates and signed tokens for the tests. DIR=`dirname "$0"` CURRENT_DIR=`cd "$DIR" && pwd` @@ -40,9 +40,9 @@ function cleanup { function generate_ca_conf { echo ' [ req ] -default_bits = 1024 +default_bits = 2048 default_keyfile = cakey.pem -default_md = sha1 +default_md = sha256 prompt = no distinguished_name = ca_distinguished_name @@ -67,9 +67,9 @@ basicConstraints = critical,CA:true function generate_ssl_req_conf { echo ' [ req ] -default_bits = 1024 +default_bits = 2048 default_keyfile = keystonekey.pem -default_md = sha1 +default_md = sha256 prompt = no distinguished_name = distinguished_name @@ -88,9 +88,9 @@ emailAddress = keystone@openstack.org function generate_cms_signing_req_conf { echo ' [ req ] -default_bits = 1024 +default_bits = 2048 default_keyfile = keystonekey.pem -default_md = sha1 +default_md = sha256 prompt = no distinguished_name = distinguished_name @@ -122,7 +122,7 @@ private_key = $dir/private/cakey.pem default_days = 21360 default_crl_days = 30 -default_md = sha1 +default_md = sha256 policy = policy_any @@ -157,14 +157,14 @@ function check_error { function generate_ca { echo 'Generating New CA Certificate ...' - openssl req -x509 -newkey rsa:1024 -days 21360 -out $CERTS_DIR/cacert.pem -keyout $PRIVATE_DIR/cakey.pem -outform PEM -config ca.conf -nodes + openssl req -x509 -newkey rsa:2048 -sha256 -days 21360 -out $CERTS_DIR/cacert.pem -keyout $PRIVATE_DIR/cakey.pem -outform PEM -config ca.conf -nodes check_error $? } function ssl_cert_req { echo 'Generating SSL Certificate Request ...' generate_ssl_req_conf - openssl req -newkey rsa:1024 -keyout $PRIVATE_DIR/ssl_key.pem -keyform PEM -out ssl_req.pem -outform PEM -config ssl_req.conf -nodes + openssl req -newkey rsa:2048 -sha256 -keyout $PRIVATE_DIR/ssl_key.pem -keyform PEM -out ssl_req.pem -outform PEM -config ssl_req.conf -nodes check_error $? #openssl req -in req.pem -text -noout } @@ -172,7 +172,7 @@ function ssl_cert_req { function cms_signing_cert_req { echo 'Generating CMS Signing Certificate Request ...' generate_cms_signing_req_conf - openssl req -newkey rsa:1024 -keyout $PRIVATE_DIR/signing_key.pem -keyform PEM -out cms_signing_req.pem -outform PEM -config cms_signing_req.conf -nodes + openssl req -newkey rsa:2048 -sha256 -keyout $PRIVATE_DIR/signing_key.pem -keyform PEM -out cms_signing_req.pem -outform PEM -config cms_signing_req.conf -nodes check_error $? #openssl req -in req.pem -text -noout } @@ -191,32 +191,18 @@ function issue_certs { check_error $? } -function create_middleware_cert { - cp $CERTS_DIR/ssl_cert.pem $CERTS_DIR/middleware.pem - cat $PRIVATE_DIR/ssl_key.pem >> $CERTS_DIR/middleware.pem -} - function check_openssl { echo 'Checking openssl availability ...' which openssl check_error $? } +JSON_FILES="${CMS_DIR}/auth_token_revoked.json ${CMS_DIR}/auth_token_unscoped.json ${CMS_DIR}/auth_token_scoped.json ${CMS_DIR}/auth_token_scoped_expired.json ${CMS_DIR}/revocation_list.json ${CMS_DIR}/auth_v3_token_scoped.json ${CMS_DIR}/auth_v3_token_revoked.json" + function gen_sample_cms { - for json_file in "${CMS_DIR}/auth_token_revoked.json" "${CMS_DIR}/auth_token_unscoped.json" "${CMS_DIR}/auth_token_scoped.json" "${CMS_DIR}/revocation_list.json" + for json_file in $JSON_FILES do openssl cms -sign -in $json_file -nosmimecap -signer $CERTS_DIR/signing_cert.pem -inkey $PRIVATE_DIR/signing_key.pem -outform PEM -nodetach -nocerts -noattr -out ${json_file/.json/.pem} done } -check_openssl -rm_old -cleanup -setup -generate_ca -ssl_cert_req -cms_signing_cert_req -issue_certs -create_middleware_cert -gen_sample_cms -cleanup diff --git a/examples/pki/private/cakey.pem b/examples/pki/private/cakey.pem index 3db9c2255..9c204c500 100644 --- a/examples/pki/private/cakey.pem +++ b/examples/pki/private/cakey.pem @@ -1,16 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMXgnd5wlHApGxZ5 -8LrpEkHU995lT9PxtMgkp0tpFhg7R5HQw9K7TfQk5NHB28hNzf8UE/c0z2pJXggP -nAzvdx27NQeJGX5CWsi6fITZ8vH/+SxgfxxC+CE/6BkDpzw21MgBtq11vWL7XVax -NeU12Ax889U66i3CrObuCYt2mbpzAgMBAAECgYEAligxJE9CFSrcR14Zc3zSQeqe -fcFbpnXQveAyo2MHRTQWx2wobY19RjuI+DOn2IRSQbK2w+zrSLiMBonx3U8Kj8nx -A4EQ75GLJEEr81TvBoIZSJAqrowNrkXNq8W++qwjlGXRjKiBAYlKMrFvR4lij4XN -6cdB7kGdSIUmhvC20sECQQD4ebCGfsgFWnrqOrco6T9eQRTvP3+gJuqYXYLuVSTC -R4gHxT5QVXSZt/Hv3UWJ0BLDbyLzLGHf30w1AqgwsUP5AkEAy96qXq6j2+IRa5w7 -2G+KZHF5N/MK/Hyy27Jw67GBVeGQj1Dwq2ZGAJBZrfXjTtQQAGdQ7EfOTCAOzHgX -2Bx0ywJAYqfGbBBIkL+VEA0SDh9WNrE2g6u9m7P371kplEGgH7dRDmzFShYz/pin -aep8IrTHzmsBAHY9wiqh0mZkqzim2QJADTYdxkr89WfeByI1wp3f0wiDeXu3j4sp -MBGNPcjf/8fBTXhKUGEtUiYImbxggaA+dTg8x0MT/FzreJajvO6DJwJARMc6rhzv -aTlm4IgApcDPBeuz6SKex9TfvDUJpqACoFM4lMgyHADi9NrJBslxFHPP5eTiM2Ag -vI7EuW837e6raQ== +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC//gLFHKshdm/9 +4GhxrVGcfpkXl9kDL/oF5ltilI9Qk5kk8RlwmnqCeTezwOgXgQ/xyVak4AqP6Dbh +5joLWK4EZ4MlqQASLbfTjUZkwq9gLwNtwGSI31HcEWXHkR3+ux+jdTTrba/CBZLk +BYUwCrkRyRnFzajTCazb9r+YWtxjXgIcWXfeuaJPQRknfob4m42dpgwWfJIifQRn ++d0w4nv84PquQpIKiwK4Ed9KRgnvmS1zy64XpKcnrQUd4Lik1djzAniIe2IP7kyT +HttqmfOzOkNQ6LEigv9Zfv9QKwb3uprHPCRBJZ3N9hyezbk0ya3mShLz5lCNqE3H +b6JFl7UbAgMBAAECggEAVCmMm03S8utRcrBB+LsqkHiqsb3+AriwWI+/tbo8DO12 +78vFBCij1bg/o8vHsi4AiFRjaAlSd/0queJLxZeNSR77TbIE9vMVp2ZB2n/Bk19o +mF8Dc0C6SMdTn6VMydLLrsL9fMrrhhkdaFnHJeU9db97Tcu22zRdk1taZ/ZEsEXN +5Ij4GSaylemUsdi2GKWCydDje3M60rGRWiVlMAsgyLhGRjBS0dfi2VRSbjE/Mi+d +vrHGWyKfh/Dgmt5XdljubZE6fbogXtksBl/dvHytQIyYddSTYHWJoCHb0DhBV25V +/m5SNTRoBeRtTy9s7UdMxTR9mpPscWCAonCZVh3nQQKBgQDqwE80+CaYInrNlZIM +6rX3sdHgcnlP8a2gSRfkmpTQbf5os6jHBwBp3UAqTpKZd6mjrbXXel4Hj8ccZMW9 +SqReQJiJwENxhvT5Bz7YGERZbVGvchXeeQBjhOy8D+Smv85I+F+cP/7efutrzFgf +M5mwlic5z58ijJlloGzgP4GquwKBgQDRXuJFzfbjY7DS0vgoz5Gh8GTfB/EPZkNL +ULt22Zz6coMpZ+en8HBrc7gSlAjvf3C4MAon344VIaW6bK1tx4amuJVK3gbbk7Kg +9YOkXUg60WaBeX9zed+eljV3LIcgRJ10Zu5rJ8ZCLgp1DTF1kh7afmqyufU5828b +vOOpN+5pIQKBgQC7qpGnntn7tVTHFVN00A44vgcyj1E7/9D12nknYAyns8c2nKnI +smg6OY4aREYeOfN7zlsYr9KL6P0cTdNmyE0urCVFulYwY9tjWc97oarCcwpiX6nr ++H+/D3zRu0Lnq16WJzkICIEQDhbWTr4D85Rh/yfMp5ZoYE4hWGaxvxNCEQKBgQCL +bD4N8fworGhB3E95DdCTIDxr8SPr91N0wgw0NvG8Lal+Vz0CrrCOPX8kkAPrSNhN +L2Bz8QDyvXdZT6ml4yqdt2ljc7rpWc+oNBY3zA6fbHZwXfIrecsaFjkAZVyOdmLL +8wdtwAzcYUCBdgmrm2SEZ46x+fd9Ychplj2coCxZQQKBgEtdtXU93fZ6vUUTN6Rj +DbWQPxktPClfBvxEr7jfdMLO33UziVQU9ZoXpj5fplWFtjLw09W1bNa6VYKGkcVK +ZYCHni/J440p7v9NvHZHX8kqYkFvzs0djFXzkLTzSKk6ErDMjvWUcQq7IvB3JLNo +xrS0SJBTBskGUKX/1OEP69B1 -----END PRIVATE KEY----- diff --git a/examples/pki/private/signing_key.pem b/examples/pki/private/signing_key.pem index b6ad7102c..ddf3066d5 100644 --- a/examples/pki/private/signing_key.pem +++ b/examples/pki/private/signing_key.pem @@ -1,16 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALqEAuiAajMQufOO -XP2ZC7HXEG0x6iKWCRxJMBKMcnhI8IhhX4mZomfdaUhTXPTH43SZzfm3v53VUtfc -NZ66yb1f7nVbH+iWVpEz1+R8sZ5ZcM5wh6T+4H7zGcxrKs1pUnYg+pnEWwmEV/YQ -gmrht44QrSEiDQlWDGEBh+kEom/bAgMBAAECgYBywfSUHya4gqsW2toGQps6cauu -s85uN0glujY0w2tO7Pnpv5errvaI12cG1BvWlAIz5MohwlfIgc919wyavCyRJgQN -xQo5v5MEMYKKc8ppmXpRr03HLwoPLOHVs6UHRJQT9dhOBfmLzMZIP7P/lJlt2/1X -Okwxft/PWorczKX1aQJBAORlVqP+Cj4r5kz1A77agnCvINioV1VM5n9PvzPVzYLH -5r1I53RWFooy1Hx2RUCmtSRQMZMeI9iGMg9c8d3LJ4UCQQDRDuIAd3AoNBcwXKC4 -BPNkbI9BSqnpIdZo87BzpY8rJ/ra3VHMHuq4w+gQsmmEy3pp01AZd1uBqv3s1wHy -muffAkEAn2ZmiH+lUGy9B5q8qXfBL7naF7utb/gCqnnSvO+LxamUTSjTeKsYgg0l -pVO503xF0fkyEDYp2FUYHQbGOwAtLQJAHkJ3N/YRx9/yU0+0+63LxQdpnNu/yDzb -mglbywF1vZtl1fQe+NqowuGoX3JTj6McLuElQOpj1lr3siZU49bEJQJBANRazUzj -Xfoja7wGuZ3PwHdxxoNDlJ2u0rYjcfK9VZuPGSz/25iCOkaar3OralJ3lfCWbFKA -vvRp8Hl2Yk4hdKM= +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCexnPwhsvNj1jp +pRg/gvHeisN0NWaHTs/RHboV3nHyzfsx13fUHd+X+YR3+HPI9MgMSPKn3mm45znn +xJKBwuFvg007FMKtcexbwDx2053Eh83j6gRPMCqeKXxbmQITMT+eZ0yQvzfLdTNZ +VKmEwDYEwgMrLcjibi7jHNN1AajvGzhswibAHXLAT6XWJH8nK1r+WaLHWh39zpmp +D7Vspo0j0PP5TOMeRZuRmqGFKm/ZqG7rDKdLXuzDJM1mIpUqmQVh8e3bBkex6DeM +zKmpbbQtIpyRnNMJ7rDa5Hm94nrl8FOk91d6RN8k07h8uh3GWXf/Jtzvy/RdaHcw +51B9dD45AgMBAAECggEAVYj/6LIVlTYGZkiEmaKHfqYuyaoDBB3XIwbquuFNbcq9 +6onzihhV3l+Tl7YHWllUdBnQb9MIDY6zyUJC0xkTramEr7Ftd1cKSBt192Xldnza +1E+75pVCQFaFIit5zLEZXtKzkr8Q5dDLyvIrKNMLxuBmKJrPv/wv0jYzTLOKONUO +BRxEb2c13lvq/RqT4xpxU8wadk/tC4N1tvLGdUnq/+1+GcmswwChBPnYafY+pzF3 +ylyoQUtirIZ0TAkSBwZZGkZC137OPs6CmbBqxLcPT2swUSQpUSAglwG7PklRAW1u +2Qin1gka7J3l6erIYfeJ87C7day+3+uGtlYxplMRIQKBgQDJobPdn/YZakdI4ZaH +YNL67qtO5A410vb0Sm6UXWASAyfRq7nMG1CyWvlfTpPTeao9bBD0oKKIteW7RTo5 +mSozpKOt2Rgb3auyOkuswFbSxRuufgRDayUnyYMaHfhfp/pHEpXg/aiwNckiZ6Mi +iOwIlLN+ytyzadfXZSrlPtVpvQKBgQDJlnD1hnOtWRgWeB4uUXzJPa9MJBK7JLRW ++FSMrUxssw14vScmLO3kV/pN1J++FMkq7Y5ZmOpy34sGc3iMaM+H4JuNCvP5SwtG +626rOm5enH2sKd4BubiZcln5zrjgw/RV/a7aQQep4cQpaEgNoyTAu6BuU0fid6xc +jVQJAXnILQKBgQDII7YB2vHRMGkpsqJUJovFgHqSiFSCoLF4sxkoM7dUqcUwniCC +tOpY32yAaeLaGv4ckdQSvhAXW1Z5mLG+0oXNVTMTMVZ48oOnGa5b/18vP2/GuFdL +BGORJrj3h6AucvI+8ffLqH10yy6m8/A+K2L+8Xtp87s2a21P5J+7ha8YkQKBgB4m +fBqc02xX6PxjVtBCq9FFgpR2yL5ozPg9CBhKSyXu2dL3J4XULnh6mBtP89xwK25a +PXI1Jsurl5WNa7hEbNW7yEgeHUNp7/PZfqHpiVxpN3qqgGPtrSh2K/Lq8kfbxw2d +dat7EnRcKgSvbidsATE6XtJhblz23TayhKEcMWS5AoGBAItYfoHpgnQXk1gXal6e +incHFVTgrUQlDKZ5UQn5/HvK1Gb3mAtPbLWJZ2Ej70lW5BQnBrCLI8188Z6PJY3w +8KJ4T2KOu+9qClnezLy2A+bZ+MU+XZrVv+65vjJ5oGhARmRLmYlasBmxwUEEKqlO +ovIe7UVLD9CyaB+MFxFpFwE6 -----END PRIVATE KEY----- diff --git a/examples/pki/private/ssl_key.pem b/examples/pki/private/ssl_key.pem index 2e4667f15..d339ea213 100644 --- a/examples/pki/private/ssl_key.pem +++ b/examples/pki/private/ssl_key.pem @@ -1,16 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALVu4bjaOH33yAx0 -WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+cUriQtxf6ahjxtzLPERS81SnwZmrICWZ -ngbOn733pULMTZktTJH+o7C74NdKwUSNxjlCeWUy+FqIQoje4ygoJRPpMdkp1wHN -O0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMBAAECgYAmwq6EYFJrTvE0//JmN/8qzfvg -dI5PoWpD+F8UInUxr2T2tHOdrOLd07vGVrKYXu7cJeCIOGKa4r02azAggioL/nE9 -FgPpqEC+QROvLuhFsk1gLZ2pGQ06sveKZVMH22h59BKZkYlhjh5qd4vlmhPqkmPp -gdXj7ZjDCJhhQdFVkQJBANp18k2mVksn8q29LMieVTSIZNN3ucDA1QHbim+3fp/O -GxCzU7Mv1Xfnu1zoRFu5/sF3YG0Zy3TGPDrEljBC3rUCQQDUnBjVFXL35OkBZqXW -taJPzGbsPoqAO+Ls2juS97zNzeGxUNhvcKuEvHO63PXqDxp1535DpvJEBN1rT2FF -iaO1AkEAt/QTWWFUTqrPxY6DNFdm5fpn9E1fg7icZJkKBDJeFJCH59MpCryfovzl -n0ERtq9ynlQ4RQYwdR8rvkylLvRP9QJAOiXHFOAc5XeR0nREfwiGL9TzgUFJl/DJ -C4ZULMnctVzNkTVPPItQHal87WppR26CCiUZ/161e6zo8eRv8hjG0QJABWqfYQuK -dWH8nxlXS+NFUDbsCdL+XpOVE7iEH7hvSw/A/kz40mLx8sDp/Fz1ysrogR/L+NGC -Vrlwm4q/WYJO0Q== +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD1i9+ydZSypNAk +kVXdzIqZ8E62cqH7i0JGVGBuGdH8ZVF3MDcbi8VwqfNRWoWn9mrJUp5HYDV9t5WX +z25Ej4EnqlJ3WLZvC1e+ldDIInmiULic3iIAgrWbumU3XNLHska/smoVJuLIuFUx +EfRpwGOpguOzAO1M6BKCSwr+TBLYJZxc3F7v1vtwNhisyE5S2H6Q49K0UXHTPjp+ +fLZAHQ5+Yxqwf0KAJqAD3vMo8EwxXlgJu8pQyjjxwtrwnN2WHYoJGt/OOdkLBbzd +upWH9CGcxeVc5hSJ1hEKuYS8AOZIeH6q2MKwT+QiepBsVfuy1JFt4raLht/RcX/W +R8lIAzoFAgMBAAECggEAQbeB0z1s4rMBkgfjt0z6+2A5cNMVT0FiJ3iFpnH6pVZo +i0G4PgMWgKS7nlZf1yg4RFF8UxYIuvDbdJnrpSXTJ06Ka66uhOHARh3KlwXDEBIS +lslMyF4zRM6KMFsDfrbUAJI7mhWiNJ5BDrUDeRookkGZt1rUJ/UknwJ+mri5gmdo +UYyHuelIcVZfX6mkDCKrVHCCNbdnUgWOkXtPi25n4lqjolwEeBx5y4cu4z6tkqxM +2hcnKUyj0YKScnhnTqprkbiMpPST4y8I190Q3Eo0Q8w9hvgEUa1xezfVzICScUjG +VXid7aRyq7sXGmCMWyMUAvc4+5/qanreEU1686CrMQKBgQD7FDKV1pcWGJMoL5VI +xiJ8jpDpnRIL940732ODjCQRgH4K9u40OmZ36W880q6o5ZXLLoWQIYZf5Hs1lq7f +3djqHFqqXtW7P7JU3mW5n/ejbMHlxra4uoTkjR6wUF+g2kW2y4ysI/t8HxSeaVjO +E4cJfoaNufgmgpeKa1QnLrMpqwKBgQD6W+qgx7w2dBXSkYBDhjDoRrCwmqOuWKwG +DMTDrlbu2j7hag6Xdy92CqjGAYXfhny4bpCkFLRjyrDZS4a/cObGKYYG4jVXTYfj +5cqLUiLmQFrZiK0uA7ek+qMp06FJX45iyECJlXX4gNU8e/WMfujlFLwjZ+72znQE +iTMW5xxbDwKBgEBa1vRtAmDZf66HM75peqFucVpPtjZ3By5XfcxT+VK7GpN442lj +pqwJm0d9wOLtpc1kaTuePDEMAUClFMGwvU6UYfDVSfcqxmzWbEB97h1nXPOmUWNb ++4ARY9JRZ5F1IPVPiwj8WBNibAiGfAqmGrCmS5q8FgzY4DrMc89vOuDtAoGBAJfe +aB6t6ssxcgdwwdi0LzjHoOkQdVgObBOjbTyypgNwGpLMrhtNblnxr12lkNr+Duwm +DdGqyZ57VvoJaaz5xNPSXn4QfIEAA/3H6CzJX2hDA5lP4pW2JZGLhKybtwv2Tj43 +8YZERvK+3Bs7qsFWPtqv0Ey+AGRw6knSHE65VScbAoGBAPUwAUmBZ6F22M6ftNL7 +G/qZJsP6OmDSyvJDN9SXiLvbpXQVd6RrZrni6xzwKWAFncFMVo+HWmeAavM8zxsc +a/XVSVvRj0kwg94HotHCF7/nHqYIqyF7hLZipdIphVGXx+c8dbKeVg0fgTnLRkzx +tbxgs3HWa6pgQvxtVAN4Jl3W -----END PRIVATE KEY----- diff --git a/examples/pki/run_all.sh b/examples/pki/run_all.sh new file mode 100755 index 000000000..2438ec7c8 --- /dev/null +++ b/examples/pki/run_all.sh @@ -0,0 +1,30 @@ +#!/bin/bash -x + +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# This script generates the crypto necessary for the SSL tests. + +. gen_pki.sh + +check_openssl +rm_old +cleanup +setup +generate_ca +ssl_cert_req +cms_signing_cert_req +issue_certs +gen_sample_cms +cleanup diff --git a/keystoneclient/__init__.py b/keystoneclient/__init__.py index 0b4062d79..e8ef58ebc 100644 --- a/keystoneclient/__init__.py +++ b/keystoneclient/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,23 +13,66 @@ # License for the specific language governing permissions and limitations # under the License. -import inspect -import os +"""The python bindings for the OpenStack Identity (Keystone) project. +A Client object will allow you to communicate with the Identity server. The +recommended way to get a Client object is to use +:py:func:`keystoneclient.client.Client()`. :py:func:`~.Client()` uses version +discovery to create a V3 or V2 client depending on what versions the Identity +server supports and what version is requested. -def _get_client_version(): - """Read version from versioninfo file.""" - mod_abspath = inspect.getabsfile(inspect.currentframe()) - client_path = os.path.dirname(mod_abspath) - version_path = os.path.join(client_path, 'versioninfo') +Identity V2 and V3 clients can also be created directly. See +:py:class:`keystoneclient.v3.client.Client` for the V3 client and +:py:class:`keystoneclient.v2_0.client.Client` for the V2 client. - if os.path.exists(version_path): - version = open(version_path).read().strip() - else: - version = "Unknown, couldn't find versioninfo file at %s"\ - % version_path +""" - return version +import importlib +import sys +import pbr.version -__version__ = _get_client_version() + +__version__ = pbr.version.VersionInfo('python-keystoneclient').version_string() + +__all__ = ( + # Modules + 'generic', + 'v2_0', + 'v3', + + # Packages + 'access', + 'client', + 'exceptions', + 'httpclient', + 'service_catalog', +) + + +class _LazyImporter(object): + def __init__(self, module): + self._module = module + + def __getattr__(self, name): + # NB: this is only called until the import has been done. + # These submodules are part of the API without explicit importing, but + # expensive to load, so we load them on-demand rather than up-front. + lazy_submodules = [ + 'access', + 'client', + 'exceptions', + 'generic', + 'httpclient', + 'service_catalog', + 'v2_0', + 'v3', + ] + if name in lazy_submodules: + return importlib.import_module('keystoneclient.%s' % name) + + # Return module attributes like __all__ etc. + return getattr(self._module, name) + + +sys.modules[__name__] = _LazyImporter(sys.modules[__name__]) diff --git a/keystoneclient/_discover.py b/keystoneclient/_discover.py new file mode 100644 index 000000000..a27236cdc --- /dev/null +++ b/keystoneclient/_discover.py @@ -0,0 +1,329 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The passive components to version discovery. + +The Discover object in discover.py contains functions that can create objects +on your behalf. These functions are not usable from within the keystoneclient +library because you will get dependency resolution issues. + +The Discover object in this file provides the querying components of Discovery. +This includes functions like url_for which allow you to retrieve URLs and the +raw data specified in version discovery responses. +""" + +import logging +import re + +from keystoneclient import exceptions +from keystoneclient.i18n import _ + + +_LOGGER = logging.getLogger(__name__) + + +def get_version_data(session, url, authenticated=None): + """Retrieve raw version data from a url.""" + headers = {'Accept': 'application/json'} + + resp = session.get(url, headers=headers, authenticated=authenticated) + + try: + body_resp = resp.json() + except ValueError: # nosec(cjschaef): raise a DiscoveryFailure below + pass + else: + # In the event of querying a root URL we will get back a list of + # available versions. + try: + return body_resp['versions']['values'] + except (KeyError, TypeError): # nosec(cjschaef): attempt to return + # versions dict or query the endpoint or raise a DiscoveryFailure + pass + + # Most servers don't have a 'values' element so accept a simple + # versions dict if available. + try: + return body_resp['versions'] + except KeyError: # nosec(cjschaef): query the endpoint or raise a + # DiscoveryFailure + pass + + # Otherwise if we query an endpoint like /v2.0 then we will get back + # just the one available version. + try: + return [body_resp['version']] + except KeyError: # nosec(cjschaef): raise a DiscoveryFailure + pass + + err_text = resp.text[:50] + '...' if len(resp.text) > 50 else resp.text + msg = _('Invalid Response - Bad version data returned: %s') % err_text + raise exceptions.DiscoveryFailure(msg) + + +def normalize_version_number(version): + """Turn a version representation into a tuple.""" + # trim the v from a 'v2.0' or similar + try: + version = version.lstrip('v') + except AttributeError: # nosec(cjschaef): 'version' is not a str, try a + # different type or raise a TypeError + pass + + # if it's an integer or a numeric as a string then normalize it + # to a string, this ensures 1 decimal point + try: + num = float(version) + except Exception: # nosec(cjschaef): 'version' is not a float, try a + # different type or raise a TypeError + pass + else: + version = str(num) + + # if it's a string (or an integer) from above break it on . + try: + return tuple(map(int, version.split('.'))) + except Exception: # nosec(cjschaef): 'version' is not str (or an int), + # try a different type or raise a TypeError + pass + + # last attempt, maybe it's a list or iterable. + try: + return tuple(map(int, version)) + except Exception: # nosec(cjschaef): 'version' is not an expected type, + # raise a TypeError + pass + + raise TypeError(_('Invalid version specified: %s') % version) + + +def version_match(required, candidate): + """Test that an available version satisfies the required version. + + To be suitable a version must be of the same major version as required + and be at least a match in minor/patch level. + + eg. 3.3 is a match for a required 3.1 but 4.1 is not. + + :param tuple required: the version that must be met. + :param tuple candidate: the version to test against required. + + :returns: True if candidate is suitable False otherwise. + :rtype: bool + """ + # major versions must be the same (e.g. even though v2 is a lower + # version than v3 we can't use it if v2 was requested) + if candidate[0] != required[0]: + return False + + # prevent selecting a minor version less than what is required + if candidate < required: + return False + + return True + + +class Discover(object): + + CURRENT_STATUSES = ('stable', 'current', 'supported') + DEPRECATED_STATUSES = ('deprecated',) + EXPERIMENTAL_STATUSES = ('experimental',) + + def __init__(self, session, url, authenticated=None): + self._data = get_version_data(session, url, + authenticated=authenticated) + + def raw_version_data(self, allow_experimental=False, + allow_deprecated=True, allow_unknown=False): + """Get raw version information from URL. + + Raw data indicates that only minimal validation processing is performed + on the data, so what is returned here will be the data in the same + format it was received from the endpoint. + + :param bool allow_experimental: Allow experimental version endpoints. + :param bool allow_deprecated: Allow deprecated version endpoints. + :param bool allow_unknown: Allow endpoints with an unrecognised status. + + :returns: The endpoints returned from the server that match the + criteria. + :rtype: list + """ + versions = [] + for v in self._data: + try: + status = v['status'] + except KeyError: + _LOGGER.warning('Skipping over invalid version data. ' + 'No stability status in version.') + continue + + status = status.lower() + + if status in self.CURRENT_STATUSES: + versions.append(v) + elif status in self.DEPRECATED_STATUSES: + if allow_deprecated: + versions.append(v) + elif status in self.EXPERIMENTAL_STATUSES: + if allow_experimental: + versions.append(v) + elif allow_unknown: + versions.append(v) + + return versions + + def version_data(self, **kwargs): + """Get normalized version data. + + Return version data in a structured way. + + :returns: A list of version data dictionaries sorted by version number. + Each data element in the returned list is a dictionary + consisting of at least: + + :version tuple: The normalized version of the endpoint. + :url str: The url for the endpoint. + :raw_status str: The status as provided by the server + :rtype: list(dict) + """ + if kwargs.pop('unstable', None): + kwargs.setdefault('allow_experimental', True) + kwargs.setdefault('allow_unknown', True) + data = self.raw_version_data(**kwargs) + versions = [] + + for v in data: + try: + version_str = v['id'] + except KeyError: + _LOGGER.info('Skipping invalid version data. Missing ID.') + continue + + try: + links = v['links'] + except KeyError: + _LOGGER.info('Skipping invalid version data. Missing links') + continue + + version_number = normalize_version_number(version_str) + + for link in links: + try: + rel = link['rel'] + url = link['href'] + except (KeyError, TypeError): + _LOGGER.info('Skipping invalid version link. ' + 'Missing link URL or relationship.') + continue + + if rel.lower() == 'self': + break + else: + _LOGGER.info('Skipping invalid version data. ' + 'Missing link to endpoint.') + continue + + versions.append({'version': version_number, + 'url': url, + 'raw_status': v['status']}) + + versions.sort(key=lambda v: v['version']) + return versions + + def data_for(self, version, **kwargs): + """Return endpoint data for a version. + + :param tuple version: The version is always a minimum version in the + same major release as there should be no compatibility issues with + using a version newer than the one asked for. + + :returns: the endpoint data for a URL that matches the required version + (the format is described in version_data) or None if no + match. + :rtype: dict + """ + version = normalize_version_number(version) + version_data = self.version_data(**kwargs) + + for data in reversed(version_data): + if version_match(version, data['version']): + return data + + return None + + def url_for(self, version, **kwargs): + """Get the endpoint url for a version. + + :param tuple version: The version is always a minimum version in the + same major release as there should be no compatibility issues with + using a version newer than the one asked for. + + :returns: The url for the specified version or None if no match. + :rtype: str + """ + data = self.data_for(version, **kwargs) + return data['url'] if data else None + + +class _VersionHacks(object): + """A container to abstract the list of version hacks. + + This could be done as simply a dictionary but is abstracted like this to + make for easier testing. + """ + + def __init__(self): + self._discovery_data = {} + + def add_discover_hack(self, service_type, old, new=''): + """Add a new hack for a service type. + + :param str service_type: The service_type in the catalog. + :param re.RegexObject old: The pattern to use. + :param str new: What to replace the pattern with. + """ + hacks = self._discovery_data.setdefault(service_type, []) + hacks.append((old, new)) + + def get_discover_hack(self, service_type, url): + """Apply the catalog hacks and figure out an unversioned endpoint. + + :param str service_type: the service_type to look up. + :param str url: The original url that came from a service_catalog. + + :returns: Either the unversioned url or the one from the catalog + to try. + """ + for old, new in self._discovery_data.get(service_type, []): + new_string, number_of_subs_made = old.subn(new, url) + if number_of_subs_made > 0: + return new_string + + return url + + +_VERSION_HACKS = _VersionHacks() +_VERSION_HACKS.add_discover_hack('identity', re.compile('/v2.0/?$'), '/') + + +def get_catalog_discover_hack(service_type, url): + """Apply the catalog hacks and figure out an unversioned endpoint. + + This function is internal to keystoneclient. + + :param str service_type: the service_type to look up. + :param str url: The original url that came from a service_catalog. + + :returns: Either the unversioned url or the one from the catalog to try. + """ + return _VERSION_HACKS.get_discover_hack(service_type, url) diff --git a/keystoneclient/access.py b/keystoneclient/access.py index 3947b6f10..a93da0a1e 100644 --- a/keystoneclient/access.py +++ b/keystoneclient/access.py @@ -16,28 +16,92 @@ import datetime +import warnings -from keystoneclient.openstack.common import timeutils +from oslo_utils import timeutils + +from keystoneclient.i18n import _ +from keystoneclient import service_catalog # gap, in seconds, to determine whether the given token is about to expire -STALE_TOKEN_DURATION = '30' +STALE_TOKEN_DURATION = 30 class AccessInfo(dict): - """An object for encapsulating a raw authentication token from keystone - and helper methods for extracting useful values from that token.""" + """Encapsulates a raw authentication token from keystone. + + Provides helper methods for extracting useful values from that token. + + """ + + @classmethod + def factory(cls, resp=None, body=None, region_name=None, auth_token=None, + **kwargs): + """Factory function to create a new AccessInfo object. + + Create AccessInfo object given a successful auth response & body + or a user-provided dict. + + .. warning:: + + Use of the region_name argument is deprecated as of the 1.7.0 + release and may be removed in the 2.0.0 release. + + """ + if region_name: + warnings.warn( + 'Use of the region_name argument is deprecated as of the ' + '1.7.0 release and may be removed in the 2.0.0 release.', + DeprecationWarning) + + if body is not None or len(kwargs): + if AccessInfoV3.is_valid(body, **kwargs): + if resp and not auth_token: + auth_token = resp.headers['X-Subject-Token'] + # NOTE(jamielennox): these return AccessInfo because they + # already have auth_token installed on them. + if body: + if region_name: + body['token']['region_name'] = region_name + return AccessInfoV3(auth_token, **body['token']) + else: + return AccessInfoV3(auth_token, **kwargs) + elif AccessInfoV2.is_valid(body, **kwargs): + if body: + if region_name: + body['access']['region_name'] = region_name + auth_ref = AccessInfoV2(**body['access']) + else: + auth_ref = AccessInfoV2(**kwargs) + else: + raise NotImplementedError(_('Unrecognized auth response')) + else: + auth_ref = AccessInfoV2(**kwargs) + + if auth_token: + auth_ref.auth_token = auth_token + + return auth_ref def __init__(self, *args, **kwargs): - dict.__init__(self, *args, **kwargs) + super(AccessInfo, self).__init__(*args, **kwargs) + self.service_catalog = service_catalog.ServiceCatalog.factory( + resource_dict=self, region_name=self._region_name) + + @property + def _region_name(self): + return self.get('region_name') def will_expire_soon(self, stale_duration=None): - """ Determines if expiration is about to occur. + """Determine if expiration is about to occur. - :return: boolean : true if expiration is within the given duration + :returns: true if expiration is within the given duration + :rtype: boolean """ - stale_duration = stale_duration or TALE_TOKEN_DURATION + stale_duration = (STALE_TOKEN_DURATION if stale_duration is None + else stale_duration) norm_expires = timeutils.normalize_time(self.expires) # (gyee) should we move auth_token.will_expire_soon() to timeutils # instead of duplicating code here? @@ -45,132 +109,778 @@ def will_expire_soon(self, stale_duration=None): seconds=stale_duration)) return norm_expires < soon - @property - def expires(self): - """ Returns the token expiration (as datetime object) + @classmethod + def is_valid(cls, body, **kwargs): + """Determine if processing valid v2 or v3 token. - :returns: datetime + Validates from the auth body or a user-provided dict. + :returns: true if auth body matches implementing class + :rtype: boolean """ - return timeutils.parse_isotime(self['token']['expires']) + raise NotImplementedError() + + def has_service_catalog(self): + """Return true if the authorization token has a service catalog. + + :returns: boolean + """ + raise NotImplementedError() @property def auth_token(self): - """ Returns the token_id associated with the auth request, to be used - in headers for authenticating OpenStack API requests. + """Return the token_id associated with the auth request. + + To be used in headers for authenticating OpenStack API requests. :returns: str """ - return self['token'].get('id', None) + return self['auth_token'] + + @auth_token.setter + def auth_token(self, value): + self['auth_token'] = value + + @auth_token.deleter + def auth_token(self): + try: + del self['auth_token'] + except KeyError: # nosec(cjschaef): 'auth_token' is not in the dict + pass + + @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): - """ Returns the username associated with the authentication request. + """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 """ - name = self['user'].get('name', None) - if name: - return name - else: - return self['user'].get('username', None) + raise NotImplementedError() @property def user_id(self): - """ Returns the user id associated with the authentication request. + """Return the user id associated with the auth request. :returns: str """ - return self['user'].get('id', None) + raise NotImplementedError() @property - def tenant_name(self): - """ Returns the tenant (project) name associated with the - authentication request. + def user_domain_id(self): + """Return the user's domain id associated with the auth request. + + For v2, it always returns 'default' which may be different from the + Keystone configuration. :returns: str """ - tenant_dict = self['token'].get('tenant', None) - if tenant_dict: - return tenant_dict.get('name', None) - return None + raise NotImplementedError() + + @property + def user_domain_name(self): + """Return the user's domain name associated with the auth request. + + For v2, it always returns 'Default' which may be different from the + Keystone configuration. + + :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): - """ Synonym for tenant_name """ - return self.tenant_name + """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): - """ Returns true if the authorization token was scoped to a tenant - (project), and contains a populated service catalog. + """Return true if the auth token was scoped. + + Return true if scoped to a tenant(project) or domain, + and contains a populated service catalog. + + .. warning:: + + This is deprecated as of the 1.7.0 release in favor of + project_scoped and may be removed in the 2.0.0 release. :returns: bool """ - if ('serviceCatalog' in self - and self['serviceCatalog'] - and 'tenant' in self['token']): - return True - return False + raise NotImplementedError() + + @property + def project_scoped(self): + """Return true if the auth token was scoped to a tenant(project). + + :returns: bool + """ + raise NotImplementedError() + + @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): - """ Returns the tenant (project) id associated with the authentication - request, or None if the authentication request wasn't scoped to a - tenant (project). + """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. + + For v2, it returns 'default' if a project is scoped or None which may + be different from the keystone configuration. :returns: str """ - tenant_dict = self['token'].get('tenant', None) - if tenant_dict: - return tenant_dict.get('id', None) - return None + raise NotImplementedError() @property - def project_id(self): - """ Synonym for project_id """ - return self.tenant_id + def project_domain_name(self): + """Return the project's domain name associated with the auth request. + + For v2, it returns 'Default' if a project is scoped or None which may + be different from the keystone configuration. + + :returns: str + """ + raise NotImplementedError() @property def auth_url(self): - """ Returns a tuple of URLs from publicURL and adminURL for the service + """Return a tuple of identity URLs. + + The identity URLs are from publicURL and adminURL for the service 'identity' from the service catalog associated with the authorization request. If the authentication request wasn't scoped to a tenant (project), this property will return None. + DEPRECATED: this doesn't correctly handle region name. You should fetch + it from the service catalog yourself. This may be removed in the 2.0.0 + release. + :returns: tuple of urls """ - return_list = [] - if 'serviceCatalog' in self and self['serviceCatalog']: - identity_services = [x for x in self['serviceCatalog'] - if x['type'] == 'identity'] - for svc in identity_services: - for endpoint in svc['endpoints']: - if 'publicURL' in endpoint: - return_list.append(endpoint['publicURL']) - if len(return_list) > 0: - return tuple(return_list) - return None + raise NotImplementedError() @property def management_url(self): - """ Returns the first adminURL for 'identity' from the service catalog + """Return the first adminURL of the identity endpoint. + + The identity endpoint is from the service catalog associated with the authorization request, or None if the authentication request wasn't scoped to a tenant (project). + DEPRECATED: this doesn't correctly handle region name. You should fetch + it from the service catalog yourself. This may be removed in the 2.0.0 + release. + :returns: tuple of urls """ - return_list = [] - if 'serviceCatalog' in self and self['serviceCatalog']: - identity_services = [x for x in self['serviceCatalog'] - if x['type'] == 'identity'] - for svc in identity_services: - for endpoint in svc['endpoints']: - if 'adminURL' in endpoint: - return_list.append(endpoint['adminURL']) - if len(return_list) > 0: - return tuple(return_list) + raise NotImplementedError() + + @property + def version(self): + """Return the version of the auth token from identity service. + + :returns: str + """ + return self.get('version') + + @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 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 + + +class AccessInfoV2(AccessInfo): + """An object for encapsulating raw v2 auth token from identity service.""" + + def __init__(self, *args, **kwargs): + super(AccessInfo, self).__init__(*args, **kwargs) + self.update(version='v2.0') + self.service_catalog = service_catalog.ServiceCatalog.factory( + resource_dict=self, + token=self['token']['id'], + region_name=self._region_name) + + @classmethod + def is_valid(cls, body, **kwargs): + if body: + return 'access' in body + elif kwargs: + return kwargs.get('version') == 'v2.0' + else: + return False + + def has_service_catalog(self): + return 'serviceCatalog' in self + + @AccessInfo.auth_token.getter + def auth_token(self): + try: + return super(AccessInfoV2, self).auth_token + except KeyError: + return self['token']['id'] + + @property + def expires(self): + return timeutils.parse_isotime(self['token']['expires']) + + @property + def issued(self): + return timeutils.parse_isotime(self['token']['issued_at']) + + @property + def username(self): + return self['user'].get('name', self['user'].get('username')) + + @property + def user_id(self): + return self['user']['id'] + + @property + def user_domain_id(self): + return 'default' + + @property + def user_domain_name(self): + return 'Default' + + @property + def role_ids(self): + return self.get('metadata', {}).get('roles', []) + + @property + 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: # nosec(cjschaef): no 'token' key or 'tenant' key in + # token, return the name of the tenant or None + pass + else: + return tenant_dict.get('name') + + # pre grizzly + try: + return self['user']['tenantName'] + except KeyError: # nosec(cjschaef): no 'user' key or 'tenantName' in + # 'user', attempt 'tenantId' or return None + pass + + # pre diablo, keystone only provided a tenantId + try: + return self['token']['tenantId'] + except KeyError: # nosec(cjschaef): no 'token' key or 'tenantName' or + # 'tenantId' could be found, return None + pass + + @property + def scoped(self): + """Deprecated as of the 1.7.0 release. + + Use project_scoped instead. It may be removed in the + 2.0.0 release. + """ + warnings.warn( + 'scoped is deprecated as of the 1.7.0 release in favor of ' + 'project_scoped and may be removed in the 2.0.0 release.', + DeprecationWarning) + if ('serviceCatalog' in self and + self['serviceCatalog'] and + 'tenant' in self['token']): + return True + return False + + @property + def project_scoped(self): + return 'tenant' in self['token'] + + @property + def domain_scoped(self): + return False + + @property + def trust_id(self): + return self.get('trust', {}).get('id') + + @property + def trust_scoped(self): + return 'trust' in self + + @property + def trustee_user_id(self): + return self.get('trust', {}).get('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: # nosec(cjschaef): no 'token' key or 'tenant' dict, + # attempt to return 'tenantId' or return None + pass + else: + return tenant_dict.get('id') + + # pre grizzly + try: + return self['user']['tenantId'] + except KeyError: # nosec(cjschaef): no 'user' key or 'tenantId' in + # 'user', attempt to retrieve from 'token' or return None + pass + + # pre diablo + try: + return self['token']['tenantId'] + except KeyError: # nosec(cjschaef): no 'token' key or 'tenantId' + # could be found, return None + pass + + @property + def project_domain_id(self): + if self.project_id: + return 'default' + + @property + def project_domain_name(self): + if self.project_id: + return 'Default' + + @property + def auth_url(self): + """Deprecated as of the 1.7.0 release. + + Use service_catalog.get_urls() instead. It may be removed in the + 2.0.0 release. + """ + warnings.warn( + 'auth_url is deprecated as of the 1.7.0 release in favor of ' + 'service_catalog.get_urls() and may be removed in the 2.0.0 ' + 'release.', DeprecationWarning) + if self.service_catalog: + return self.service_catalog.get_urls(service_type='identity', + endpoint_type='publicURL', + region_name=self._region_name) + else: + return None + + @property + def management_url(self): + """Deprecated as of the 1.7.0 release. + + Use service_catalog.get_urls() instead. It may be removed in the + 2.0.0 release. + """ + warnings.warn( + 'management_url is deprecated as of the 1.7.0 release in favor of ' + 'service_catalog.get_urls() and may be removed in the 2.0.0 ' + 'release.', DeprecationWarning) + if self.service_catalog: + return self.service_catalog.get_urls(service_type='identity', + endpoint_type='adminURL', + region_name=self._region_name) + else: + return None + + @property + def oauth_access_token_id(self): + return None + + @property + def oauth_consumer_id(self): + return None + + @property + def is_federated(self): + return False + + @property + def audit_id(self): + try: + return self['token'].get('audit_ids', [])[0] + except IndexError: + return None + + @property + def audit_chain_id(self): + try: + return self['token'].get('audit_ids', [])[1] + except IndexError: + return None + + +class AccessInfoV3(AccessInfo): + """An object encapsulating raw v3 auth token from identity service.""" + + def __init__(self, token, *args, **kwargs): + super(AccessInfo, self).__init__(*args, **kwargs) + self.update(version='v3') + self.service_catalog = service_catalog.ServiceCatalog.factory( + resource_dict=self, + token=token, + region_name=self._region_name) + if token: + self.auth_token = token + + @classmethod + def is_valid(cls, body, **kwargs): + if body: + return 'token' in body + elif kwargs: + return kwargs.get('version') == 'v3' + else: + return False + + def has_service_catalog(self): + return 'catalog' in self + + @property + def is_federated(self): + return 'OS-FEDERATION' in self['user'] + + @property + def expires(self): + return timeutils.parse_isotime(self['expires_at']) + + @property + def issued(self): + return timeutils.parse_isotime(self['issued_at']) + + @property + 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 + + @property + def role_ids(self): + return [r['id'] for r in self.get('roles', [])] + + @property + def role_names(self): + return [r['name'] for r in self.get('roles', [])] + + @property + def username(self): + return self['user']['name'] + + @property + def domain_name(self): + domain = self.get('domain') + if domain: + return domain['name'] + + @property + def domain_id(self): + domain = self.get('domain') + if domain: + return domain['id'] + + @property + def project_id(self): + project = self.get('project') + if project: + return project['id'] + + @property + def project_domain_id(self): + project = self.get('project') + if project: + return project['domain']['id'] + + @property + def project_domain_name(self): + project = self.get('project') + if project: + return project['domain']['name'] + + @property + def project_name(self): + project = self.get('project') + if project: + return project['name'] + + @property + def scoped(self): + """Deprecated as of the 1.7.0 release. + + Use project_scoped instead. It may be removed in the + 2.0.0 release. + """ + warnings.warn( + 'scoped is deprecated as of the 1.7.0 release in favor of ' + 'project_scoped and may be removed in the 2.0.0 release.', + DeprecationWarning) + return ('catalog' in self and self['catalog'] and 'project' in self) + + @property + def project_scoped(self): + return 'project' in self + + @property + def domain_scoped(self): + return 'domain' in self + + @property + def trust_id(self): + return self.get('OS-TRUST:trust', {}).get('id') + + @property + def trust_scoped(self): + return 'OS-TRUST:trust' in self + + @property + def trustee_user_id(self): + return self.get('OS-TRUST:trust', {}).get('trustee_user', {}).get('id') + + @property + def trustor_user_id(self): + return self.get('OS-TRUST:trust', {}).get('trustor_user', {}).get('id') + + @property + def auth_url(self): + """Deprecated as of the 1.7.0 release. + + Use service_catalog.get_urls() instead. It may be removed in the + 2.0.0 release. + """ + warnings.warn( + 'auth_url is deprecated as of the 1.7.0 release in favor of ' + 'service_catalog.get_urls() and may be removed in the 2.0.0 ' + 'release.', DeprecationWarning) + if self.service_catalog: + return self.service_catalog.get_urls(service_type='identity', + endpoint_type='public', + region_name=self._region_name) + else: + return None + + @property + def management_url(self): + """Deprecated as of the 1.7.0 release. + + Use service_catalog.get_urls() instead. It may be removed in the + 2.0.0 release. + """ + warnings.warn( + 'management_url is deprecated as of the 1.7.0 release in favor of ' + 'service_catalog.get_urls() and may be removed in the 2.0.0 ' + 'release.', DeprecationWarning) + if self.service_catalog: + return self.service_catalog.get_urls(service_type='identity', + endpoint_type='admin', + region_name=self._region_name) + + else: + return None + + @property + def oauth_access_token_id(self): + return self.get('OS-OAUTH1', {}).get('access_token_id') + + @property + def oauth_consumer_id(self): + return self.get('OS-OAUTH1', {}).get('consumer_id') + + @property + def audit_id(self): + try: + return self.get('audit_ids', [])[0] + except IndexError: + return None + + @property + def audit_chain_id(self): + try: + return self.get('audit_ids', [])[1] + except IndexError: + return None diff --git a/keystoneclient/adapter.py b/keystoneclient/adapter.py new file mode 100644 index 000000000..94cd81dc6 --- /dev/null +++ b/keystoneclient/adapter.py @@ -0,0 +1,221 @@ +# 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 warnings + +from oslo_serialization import jsonutils + + +class Adapter(object): + """An instance of a session with local variables. + + A session is a global object that is shared around amongst many clients. It + therefore contains state that is relevant to everyone. There is a lot of + state such as the service type and region_name that are only relevant to a + particular client that is using the session. An adapter provides a wrapper + of client local data around the global session object. + + :param session: The session object to wrap. + :type session: keystoneclient.session.Session + :param str service_type: The default service_type for URL discovery. + :param str service_name: The default service_name for URL discovery. + :param str interface: The default interface for URL discovery. + :param str region_name: The default region_name for URL discovery. + :param str endpoint_override: Always use this endpoint URL for requests + for this client. + :param tuple version: The version that this API targets. + :param auth: An auth plugin to use instead of the session one. + :type auth: keystoneclient.auth.base.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 + """ + + 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): + warnings.warn( + 'keystoneclient.adapter.Adapter is deprecated as of the 2.1.0 ' + 'release in favor of keystoneauth1.adapter.Adapter. It will be ' + 'removed in future releases.', DeprecationWarning) + + # NOTE(jamielennox): when adding new parameters to adapter please also + # add them to the adapter call in httpclient.HTTPClient.__init__ + self.session = session + self.service_type = service_type + self.service_name = service_name + self.interface = interface + self.region_name = region_name + self.endpoint_override = endpoint_override + self.version = version + self.user_agent = user_agent + self.auth = auth + self.connect_retries = connect_retries + self.logger = logger + + def _set_endpoint_filter_kwargs(self, kwargs): + if self.service_type: + kwargs.setdefault('service_type', self.service_type) + if self.service_name: + kwargs.setdefault('service_name', self.service_name) + if self.interface: + kwargs.setdefault('interface', self.interface) + if self.region_name: + kwargs.setdefault('region_name', self.region_name) + if self.version: + kwargs.setdefault('version', self.version) + + def request(self, url, method, **kwargs): + endpoint_filter = kwargs.setdefault('endpoint_filter', {}) + self._set_endpoint_filter_kwargs(endpoint_filter) + + if self.endpoint_override: + kwargs.setdefault('endpoint_override', self.endpoint_override) + + if self.auth: + kwargs.setdefault('auth', self.auth) + if self.user_agent: + kwargs.setdefault('user_agent', self.user_agent) + if self.connect_retries is not None: + kwargs.setdefault('connect_retries', self.connect_retries) + if self.logger: + kwargs.setdefault('logger', self.logger) + + return self.session.request(url, method, **kwargs) + + def get_token(self, auth=None): + """Return a token as provided by the auth plugin. + + :param auth: The auth plugin to use for token. Overrides the plugin + on the session. (optional) + :type auth: :class:`keystoneclient.auth.base.BaseAuthPlugin` + + :raises keystoneclient.exceptions.AuthorizationFailure: if a new token + fetch fails. + + :returns: A valid token. + :rtype: string + """ + 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: :class:`keystoneclient.auth.base.BaseAuthPlugin` + + :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not + available. + + :returns: An endpoint if available or None. + :rtype: string + """ + if self.endpoint_override: + return self.endpoint_override + + self._set_endpoint_filter_kwargs(kwargs) + return self.session.get_endpoint(auth or self.auth, **kwargs) + + def invalidate(self, auth=None): + """Invalidate an authentication plugin.""" + return self.session.invalidate(auth or self.auth) + + def get_user_id(self, auth=None): + """Return the authenticated user_id as provided by the auth plugin. + + :param auth: The auth plugin to use for token. Overrides the plugin + on the session. (optional) + :type auth: keystoneclient.auth.base.BaseAuthPlugin + + :raises keystoneclient.exceptions.AuthorizationFailure: + if a new token fetch fails. + :raises keystoneclient.exceptions.MissingAuthPlugin: + if a plugin is not available. + + :returns: Current `user_id` or None if not supported by plugin. + :rtype: string + """ + 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: keystoneclient.auth.base.BaseAuthPlugin + + :raises keystoneclient.exceptions.AuthorizationFailure: + if a new token fetch fails. + :raises keystoneclient.exceptions.MissingAuthPlugin: + if a plugin is not available. + + :returns: Current `project_id` or None if not supported by plugin. + :rtype: string + """ + 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) + + +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: # nosec(cjschaef): kwargs doesn't contain a 'body' + # key, while 'json' is an optional argument for Session.request + pass + + resp = super(LegacyJsonAdapter, self).request(*args, **kwargs) + + body = None + if resp.text: + try: + body = jsonutils.loads(resp.text) + except ValueError: # nosec(cjschaef): return None for body as + # expected + pass + + return resp, body diff --git a/keystoneclient/auth/__init__.py b/keystoneclient/auth/__init__.py new file mode 100644 index 000000000..c9acef819 --- /dev/null +++ b/keystoneclient/auth/__init__.py @@ -0,0 +1,39 @@ +# 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. + +# flake8: noqa: F405 + +from keystoneclient.auth.base import * # noqa +from keystoneclient.auth.cli import * # noqa +from keystoneclient.auth.conf import * # noqa + + +__all__ = ( + # auth.base + 'AUTH_INTERFACE', + 'BaseAuthPlugin', + 'get_available_plugin_names', + 'get_available_plugin_classes', + 'get_plugin_class', + 'IDENTITY_AUTH_HEADER_NAME', + 'PLUGIN_NAMESPACE', + + # auth.cli + 'load_from_argparse_arguments', + 'register_argparse_arguments', + + # auth.conf + 'get_common_conf_options', + 'get_plugin_options', + 'load_from_conf_options', + 'register_conf_options', +) diff --git a/keystoneclient/auth/base.py b/keystoneclient/auth/base.py new file mode 100644 index 000000000..b6753cdf1 --- /dev/null +++ b/keystoneclient/auth/base.py @@ -0,0 +1,373 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +from debtcollector import removals +from keystoneauth1 import plugin +import stevedore + +from keystoneclient import exceptions + + +# NOTE(jamielennox): The AUTH_INTERFACE is a special value that can be +# requested from get_endpoint. If a plugin receives this as the value of +# 'interface' it should return the initial URL that was passed to the plugin. +AUTH_INTERFACE = plugin.AUTH_INTERFACE + +PLUGIN_NAMESPACE = 'keystoneclient.auth.plugin' +IDENTITY_AUTH_HEADER_NAME = 'X-Auth-Token' + + +@removals.remove( + message='keystoneclient auth plugins are deprecated. Use keystoneauth.', + version='2.1.0', + removal_version='3.0.0' +) +def get_available_plugin_names(): + """Get the names of all the plugins that are available on the system. + + This is particularly useful for help and error text to prompt a user for + example what plugins they may specify. + + :returns: A list of names. + :rtype: frozenset + """ + mgr = stevedore.ExtensionManager(namespace=PLUGIN_NAMESPACE, + invoke_on_load=False) + return frozenset(mgr.names()) + + +@removals.remove( + message='keystoneclient auth plugins are deprecated. Use keystoneauth.', + version='2.1.0', + removal_version='3.0.0' +) +def get_available_plugin_classes(): + """Retrieve all the plugin classes available on the system. + + :returns: A dict with plugin entrypoint name as the key and the plugin + class as the value. + :rtype: dict + """ + mgr = stevedore.ExtensionManager(namespace=PLUGIN_NAMESPACE, + propagate_map_exceptions=True, + invoke_on_load=False) + + return dict(mgr.map(lambda ext: (ext.entry_point.name, ext.plugin))) + + +@removals.remove( + message='keystoneclient auth plugins are deprecated. Use keystoneauth.', + version='2.1.0', + removal_version='3.0.0' +) +def get_plugin_class(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:`keystoneclient.auth.BaseAuthPlugin` + + :raises keystoneclient.exceptions.NoMatchingPlugin: if a plugin cannot be + created. + """ + try: + mgr = stevedore.DriverManager(namespace=PLUGIN_NAMESPACE, + name=name, + invoke_on_load=False) + except RuntimeError: + raise exceptions.NoMatchingPlugin(name) + + return mgr.driver + + +class BaseAuthPlugin(object): + """The basic structure of an authentication plugin.""" + + def get_token(self, session, **kwargs): + """Obtain a token. + + How the token is obtained is up to the plugin. If it is still valid + it may be re-used, retrieved from cache or invoke an authentication + request against a server. + + There are no required kwargs. They are passed directly to the auth + plugin and they are implementation specific. + + Returning None will indicate that no token was able to be retrieved. + + This function is misplaced as it should only be required for auth + plugins that use the 'X-Auth-Token' header. However due to the way + plugins evolved this method is required and often called to trigger an + authentication request on a new plugin. + + When implementing a new plugin it is advised that you implement this + method, however if you don't require the 'X-Auth-Token' header override + the `get_headers` method instead. + + :param session: A session object so the plugin can make HTTP calls. + :type session: keystoneclient.session.Session + + :return: A token to use. + :rtype: string + """ + return None + + def get_headers(self, session, **kwargs): + """Fetch authentication headers for message. + + This is a more generalized replacement of the older get_token to allow + plugins to specify different or additional authentication headers to + the OpenStack standard 'X-Auth-Token' header. + + How the authentication headers are obtained is up to the plugin. If the + headers are still valid they may be re-used, retrieved from cache or + the plugin may invoke an authentication request against a server. + + The default implementation of get_headers calls the `get_token` method + to enable older style plugins to continue functioning unchanged. + Subclasses should feel free to completely override this function to + provide the headers that they want. + + There are no required kwargs. They are passed directly to the auth + plugin and they are implementation specific. + + Returning None will indicate that no token was able to be retrieved and + that authorization was a failure. Adding no authentication data can be + achieved by returning an empty dictionary. + + :param session: The session object that the auth_plugin belongs to. + :type session: keystoneclient.session.Session + + :returns: Headers that are set to authenticate a message or None for + failure. Note that when checking this value that the empty + dict is a valid, non-failure response. + :rtype: dict + """ + token = self.get_token(session) + + if not token: + return None + + return {IDENTITY_AUTH_HEADER_NAME: token} + + def get_endpoint(self, session, **kwargs): + """Return an endpoint for the client. + + There are no required keyword arguments to ``get_endpoint`` as a plugin + implementation should use best effort with the information available to + determine the endpoint. However there are certain standard options that + will be generated by the clients and should be used by plugins: + + - ``service_type``: what sort of service is required. + - ``service_name``: the name of the service in the catalog. + - ``interface``: what visibility the endpoint should have. + - ``region_name``: the region the endpoint exists in. + + :param session: The session object that the auth_plugin belongs to. + :type session: keystoneclient.session.Session + + :returns: The base URL that will be used to talk to the required + service or None if not available. + :rtype: string + """ + return None + + def get_connection_params(self, session, **kwargs): + """Return any additional connection parameters required for the plugin. + + :param session: The session object that the auth_plugin belongs to. + :type session: keystoneclient.session.Session + + :returns: Headers that are set to authenticate a message or None for + failure. Note that when checking this value that the empty + dict is a valid, non-failure response. + :rtype: dict + """ + return {} + + def invalidate(self): + """Invalidate the current authentication data. + + This should result in fetching a new token on next call. + + A plugin may be invalidated if an Unauthorized HTTP response is + returned to indicate that the token may have been revoked or is + otherwise now invalid. + + :returns: True if there was something that the plugin did to + invalidate. This means that it makes sense to try again. If + nothing happens returns False to indicate give up. + :rtype: bool + """ + return False + + def get_user_id(self, session, **kwargs): + """Return a unique user identifier of the plugin. + + Wherever possible the user id should be inferred from the token however + there are certain URLs and other places that require access to the + currently authenticated user id. + + :param session: A session object so the plugin can make HTTP calls. + :type session: keystoneclient.session.Session + + :returns: A user identifier or None if one is not available. + :rtype: str + """ + return None + + def get_project_id(self, session, **kwargs): + """Return the project id that we are authenticated to. + + Wherever possible the project id should be inferred from the token + however there are certain URLs and other places that require access to + the currently authenticated project id. + + :param session: A session object so the plugin can make HTTP calls. + :type session: keystoneclient.session.Session + + :returns: A project identifier or None if one is not available. + :rtype: str + """ + return None + + @classmethod + def get_options(cls): + """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 [] + + @classmethod + def load_from_options(cls, **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. + """ + return cls(**kwargs) + + @classmethod + def register_argparse_arguments(cls, parser): + """Register the CLI options provided by a specific plugin. + + Given a plugin class convert it's options into argparse arguments and + add them to a parser. + + :param parser: the parser to attach argparse options. + :type parser: argparse.ArgumentParser + """ + # NOTE(jamielennox): ideally oslo_config would be smart enough to + # handle all the Opt manipulation that goes on in this file. However it + # is currently not. Options are handled in as similar a way as + # possible to oslo_config such that when available we should be able to + # transition. + + for opt in cls.get_options(): + args = [] + envs = [] + + for o in [opt] + opt.deprecated_opts: + args.append('--os-%s' % o.name) + envs.append('OS_%s' % o.name.replace('-', '_').upper()) + + # select the first ENV that is not false-y or return None + env_vars = (os.environ.get(e) for e in envs) + default = next(filter(None, env_vars), None) + + parser.add_argument(*args, + default=default or opt.default, + metavar=opt.metavar, + help=opt.help, + dest='os_%s' % opt.dest) + + @classmethod + def load_from_argparse_arguments(cls, namespace, **kwargs): + """Load a specific plugin object from an argparse result. + + Convert the results of a parse into the specified plugin. + + :param namespace: The result from CLI parsing. + :type namespace: argparse.Namespace + + :returns: An auth plugin, or None if a name is not provided. + :rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin` + """ + def _getter(opt): + return getattr(namespace, 'os_%s' % opt.dest) + + return cls.load_from_options_getter(_getter, **kwargs) + + @classmethod + def register_conf_options(cls, conf, group): + """Register the oslo_config options that are needed for a plugin. + + :param conf: A config object. + :type conf: oslo_config.cfg.ConfigOpts + :param string group: The group name that options should be read from. + """ + plugin_opts = cls.get_options() + conf.register_opts(plugin_opts, group=group) + + @classmethod + def load_from_conf_options(cls, conf, group, **kwargs): + """Load the plugin from a CONF object. + + Convert the options already registered into a real plugin. + + :param conf: A config object. + :type conf: oslo_config.cfg.ConfigOpts + :param string group: The group name that options should be read from. + + :returns: An authentication Plugin. + :rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin` + """ + def _getter(opt): + return conf[group][opt.dest] + + return cls.load_from_options_getter(_getter, **kwargs) + + @classmethod + def load_from_options_getter(cls, getter, **kwargs): + """Load a plugin from a getter function returning 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 one value, an + :py:class:`oslo_config.cfg.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:`keystoneclient.auth.BaseAuthPlugin` + """ + plugin_opts = cls.get_options() + + for opt in plugin_opts: + val = getter(opt) + if val is not None: + val = opt.type(val) + kwargs.setdefault(opt.dest, val) + + return cls.load_from_options(**kwargs) diff --git a/keystoneclient/auth/cli.py b/keystoneclient/auth/cli.py new file mode 100644 index 000000000..d18baec7e --- /dev/null +++ b/keystoneclient/auth/cli.py @@ -0,0 +1,95 @@ +# 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 debtcollector import removals + +from keystoneclient.auth import base + + +@removals.remove( + message='keystoneclient auth plugins are deprecated. Use keystoneauth.', + version='2.1.0', + removal_version='3.0.0' +) +def register_argparse_arguments(parser, argv, default=None): + """Register CLI options needed to create a plugin. + + The function inspects the provided arguments so that it can also register + the options required for that specific plugin if available. + + :param argparse.ArgumentParser: the parser to attach argparse options to. + :param List argv: the arguments provided to the application. + :param str/class default: a default plugin name or a plugin object to use + if one isn't specified by the CLI. default: None. + + :returns: The plugin class that will be loaded or None if not provided. + :rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin` + + :raises keystoneclient.exceptions.NoMatchingPlugin: if a plugin cannot be + created. + """ + in_parser = argparse.ArgumentParser(add_help=False) + env_plugin = os.environ.get('OS_AUTH_PLUGIN', default) + for p in (in_parser, parser): + p.add_argument('--os-auth-plugin', + metavar='', + default=env_plugin, + help='The auth plugin to load') + + options, _args = in_parser.parse_known_args(argv) + + if not options.os_auth_plugin: + return None + + if isinstance(options.os_auth_plugin, type): + msg = 'Default Authentication options' + plugin = options.os_auth_plugin + else: + msg = 'Options specific to the %s plugin.' % options.os_auth_plugin + plugin = base.get_plugin_class(options.os_auth_plugin) + + group = parser.add_argument_group('Authentication Options', msg) + plugin.register_argparse_arguments(group) + return plugin + + +@removals.remove( + message='keystoneclient auth plugins are deprecated. Use keystoneauth.', + version='2.1.0', + removal_version='3.0.0' +) +def load_from_argparse_arguments(namespace, **kwargs): + """Retrieve the created plugin from the completed argparse results. + + Loads and creates the auth plugin from the information parsed from the + command line by argparse. + + :param Namespace namespace: The result from CLI parsing. + + :returns: An auth plugin, or None if a name is not provided. + :rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin` + + :raises keystoneclient.exceptions.NoMatchingPlugin: if a plugin cannot be + created. + """ + if not namespace.os_auth_plugin: + return None + + if isinstance(namespace.os_auth_plugin, type): + plugin = namespace.os_auth_plugin + else: + plugin = base.get_plugin_class(namespace.os_auth_plugin) + + return plugin.load_from_argparse_arguments(namespace, **kwargs) diff --git a/keystoneclient/auth/conf.py b/keystoneclient/auth/conf.py new file mode 100644 index 000000000..ca3cbcf8c --- /dev/null +++ b/keystoneclient/auth/conf.py @@ -0,0 +1,132 @@ +# 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 debtcollector import removals +from oslo_config import cfg + +from keystoneclient.auth import base + +_AUTH_PLUGIN_OPT = cfg.StrOpt('auth_plugin', help='Name of the plugin to load') + +_section_help = 'Config Section from which to load plugin specific options' +_AUTH_SECTION_OPT = cfg.StrOpt('auth_section', help=_section_help) + + +@removals.remove( + message='keystoneclient auth plugins are deprecated. Use keystoneauth.', + version='2.1.0', + removal_version='3.0.0' +) +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_plugin: 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_PLUGIN_OPT, _AUTH_SECTION_OPT] + + +@removals.remove( + message='keystoneclient auth plugins are deprecated. Use keystoneauth.', + version='2.1.0', + removal_version='3.0.0' +) +def get_plugin_options(name): + """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. + + :returns: A list of oslo_config options. + """ + return base.get_plugin_class(name).get_options() + + +@removals.remove( + message='keystoneclient auth plugins are deprecated. Use keystoneauth.', + version='2.1.0', + removal_version='3.0.0' +) +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_plugin: 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, 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_PLUGIN_OPT, group=group) + + +@removals.remove( + message='keystoneclient auth plugins are deprecated. Use keystoneauth.', + version='2.1.0', + removal_version='3.0.0' +) +def load_from_conf_options(conf, group, **kwargs): + """Load a plugin from an oslo_config CONF object. + + Each plugin will register their own required options and so there is no + standard list and the plugin should be consulted. + + The base options should have been registered with register_conf_options + before this function is called. + + :param conf: A conf object. + :type conf: oslo_config.cfg.ConfigOpts + :param string group: The group name that options should be read from. + + :returns: An authentication Plugin or None if a name is not provided + :rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin` + + :raises keystoneclient.exceptions.NoMatchingPlugin: if a plugin cannot be + created. + """ + # NOTE(jamielennox): plugins are allowed to specify a 'section' which is + # the group that auth options should be taken from. If not present they + # come from the same as the base options were registered in. + if conf[group].auth_section: + group = conf[group].auth_section + + name = conf[group].auth_plugin + if not name: + return None + + plugin_class = base.get_plugin_class(name) + plugin_class.register_conf_options(conf, group) + return plugin_class.load_from_conf_options(conf, group, **kwargs) diff --git a/keystoneclient/auth/identity/__init__.py b/keystoneclient/auth/identity/__init__.py new file mode 100644 index 000000000..8146c1e58 --- /dev/null +++ b/keystoneclient/auth/identity/__init__.py @@ -0,0 +1,37 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.auth.identity import base +from keystoneclient.auth.identity import generic +from keystoneclient.auth.identity import v2 +from keystoneclient.auth.identity import v3 + + +BaseIdentityPlugin = base.BaseIdentityPlugin + +V2Password = v2.Password +V2Token = v2.Token + +V3Password = v3.Password +V3Token = v3.Token + +Password = generic.Password +Token = generic.Token + + +__all__ = ('BaseIdentityPlugin', + 'Password', + 'Token', + 'V2Password', + 'V2Token', + 'V3Password', + 'V3Token') diff --git a/keystoneclient/auth/identity/access.py b/keystoneclient/auth/identity/access.py new file mode 100644 index 000000000..3e096b7af --- /dev/null +++ b/keystoneclient/auth/identity/access.py @@ -0,0 +1,45 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.auth.identity import base + + +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: keystoneclient.access.AccessInfo + :param auth_url: the url where this AccessInfo was retrieved from. Required + if using the AUTH_INTERFACE with get_endpoint. (optional) + """ + + 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 diff --git a/keystoneclient/auth/identity/base.py b/keystoneclient/auth/identity/base.py new file mode 100644 index 000000000..b27d349e6 --- /dev/null +++ b/keystoneclient/auth/identity/base.py @@ -0,0 +1,418 @@ +# 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 logging +import threading +import warnings + +from oslo_config import cfg + +from keystoneclient import _discover +from keystoneclient.auth import base +from keystoneclient import exceptions + +LOG = logging.getLogger(__name__) + + +def get_options(): + return [ + cfg.StrOpt('auth-url', help='Authentication URL'), + ] + + +class BaseIdentityPlugin(base.BaseAuthPlugin, metaclass=abc.ABCMeta): + + # 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, + username=None, + password=None, + token=None, + trust_id=None, + reauthenticate=True): + + super(BaseIdentityPlugin, self).__init__() + + warnings.warn( + 'keystoneclient auth plugins are deprecated as of the 2.1.0 ' + 'release in favor of keystoneauth1 plugins. They will be removed ' + 'in future releases.', DeprecationWarning) + + self.auth_url = auth_url + self.auth_ref = None + self.reauthenticate = reauthenticate + + self._endpoint_cache = {} + self._lock = threading.Lock() + + self._username = username + self._password = password + self._token = token + self._trust_id = trust_id + + @property + def username(self): + """Deprecated as of the 1.7.0 release. + + It may be removed in the 2.0.0 release. + """ + warnings.warn( + 'username is deprecated as of the 1.7.0 release and may be ' + 'removed in the 2.0.0 release.', DeprecationWarning) + + return self._username + + @username.setter + def username(self, value): + """Deprecated as of the 1.7.0 release. + + It may be removed in the 2.0.0 release. + """ + warnings.warn( + 'username is deprecated as of the 1.7.0 release and may be ' + 'removed in the 2.0.0 release.', DeprecationWarning) + + self._username = value + + @property + def password(self): + """Deprecated as of the 1.7.0 release. + + It may be removed in the 2.0.0 release. + """ + warnings.warn( + 'password is deprecated as of the 1.7.0 release and may be ' + 'removed in the 2.0.0 release.', DeprecationWarning) + + return self._password + + @password.setter + def password(self, value): + """Deprecated as of the 1.7.0 release. + + It may be removed in the 2.0.0 release. + """ + warnings.warn( + 'password is deprecated as of the 1.7.0 release and may be ' + 'removed in the 2.0.0 release.', DeprecationWarning) + + self._password = value + + @property + def token(self): + """Deprecated as of the 1.7.0 release. + + It may be removed in the 2.0.0 release. + """ + warnings.warn( + 'token is deprecated as of the 1.7.0 release and may be ' + 'removed in the 2.0.0 release.', DeprecationWarning) + + return self._token + + @token.setter + def token(self, value): + """Deprecated as of the 1.7.0 release. + + It may be removed in the 2.0.0 release. + """ + warnings.warn( + 'token is deprecated as of the 1.7.0 release and may be ' + 'removed in the 2.0.0 release.', DeprecationWarning) + + self._token = value + + @property + def trust_id(self): + """Deprecated as of the 1.7.0 release. + + It may be removed in the 2.0.0 release. + """ + warnings.warn( + 'trust_id is deprecated as of the 1.7.0 release and may be ' + 'removed in the 2.0.0 release.', DeprecationWarning) + + return self._trust_id + + @trust_id.setter + def trust_id(self, value): + """Deprecated as of the 1.7.0 release. + + It may be removed in the 2.0.0 release. + """ + warnings.warn( + 'trust_id is deprecated as of the 1.7.0 release and may be ' + 'removed in the 2.0.0 release.', DeprecationWarning) + + self._trust_id = value + + @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 method should not be called independently and is expected to be + invoked via the do_authenticate() method. + + This method will be invoked if the AccessInfo 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: keystoneclient.session.Session + + :raises keystoneclient.exceptions.InvalidResponse: The response + returned wasn't + appropriate. + :raises keystoneclient.exceptions.HttpError: An error from an invalid + HTTP response. + + :returns: Token access information. + :rtype: :py:class:`keystoneclient.access.AccessInfo` + """ + pass # pragma: no cover + + 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: keystoneclient.session.Session + + :raises keystoneclient.exceptions.HttpError: An error from an invalid + HTTP response. + + :return: A valid token. + :rtype: string + """ + return self.get_access(session).auth_token + + def _needs_reauthenticate(self): + """Return if the existing token needs to be re-authenticated. + + The token should be refreshed if it is about to expire. + + :returns: True if the plugin should fetch a new token. False otherwise. + """ + if not self.auth_ref: + # authentication was never fetched. + return True + + if not self.reauthenticate: + # don't re-authenticate if it has been disallowed. + return False + + if self.auth_ref.will_expire_soon(self.MIN_TOKEN_LIFE_SECONDS): + # if it's about to expire we should re-authenticate now. + return True + + # otherwise it's fine and use the existing one. + return False + + def get_access(self, session, **kwargs): + """Fetch or return a current AccessInfo object. + + If a valid AccessInfo is present then it is returned otherwise a new + one will be fetched. + + :param session: A session object that can be used for communication. + :type session: keystoneclient.session.Session + + :raises keystoneclient.exceptions.HttpError: An error from an invalid + HTTP response. + + :returns: Valid AccessInfo + :rtype: :py:class:`keystoneclient.access.AccessInfo` + """ + # Hey Kids! Thread safety is important particularly in the case where + # a service is creating an admin style plugin that will then proceed + # to make calls from many threads. As a token expires all the threads + # will try and fetch a new token at once, so we want to ensure that + # only one thread tries to actually fetch from keystone at once. + with self._lock: + if self._needs_reauthenticate(): + self.auth_ref = self.get_auth_ref(session) + + return self.auth_ref + + def invalidate(self): + """Invalidate the current authentication data. + + This should result in fetching a new token on next call. + + A plugin may be invalidated if an Unauthorized HTTP response is + returned to indicate that the token may have been revoked or is + otherwise now invalid. + + :returns: True if there was something that the plugin did to + invalidate. This means that it makes sense to try again. If + nothing happens returns False to indicate give up. + :rtype: bool + """ + if self.auth_ref: + self.auth_ref = None + return True + + return False + + def get_endpoint(self, session, service_type=None, interface=None, + region_name=None, service_name=None, version=None, + **kwargs): + """Return a valid endpoint for a service. + + If a valid token is not present then a new one will be fetched using + the session and kwargs. + + :param session: A session object that can be used for communication. + :type session: keystoneclient.session.Session + :param string service_type: The type of service to lookup the endpoint + for. This plugin will return None (failure) + if service_type is not provided. + :param string interface: The exposure of the endpoint. Should be + `public`, `internal`, `admin`, or `auth`. + `auth` is special here to use the `auth_url` + rather than a URL extracted from the service + catalog. Defaults to `public`. + :param string region_name: The region the endpoint should exist in. + (optional) + :param string service_name: The name of the service in the catalog. + (optional) + :param tuple version: The minimum version number required for this + endpoint. (optional) + + :raises keystoneclient.exceptions.HttpError: An error from an invalid + HTTP response. + + :return: A valid endpoint URL or None if not available. + :rtype: string or None + """ + # NOTE(jamielennox): if you specifically ask for requests to be sent to + # the auth url then we can ignore many of the checks. Typically if you + # are asking for the auth endpoint it means that there is no catalog to + # query however we still need to support asking for a specific version + # of the auth_url for generic plugins. + if interface is base.AUTH_INTERFACE: + url = self.auth_url + service_type = service_type or 'identity' + + else: + if not service_type: + LOG.warning( + 'Plugin cannot return an endpoint without knowing the ' + 'service type that is required. Add service_type to ' + 'endpoint filtering data.') + return None + + if not interface: + interface = 'public' + + service_catalog = self.get_access(session).service_catalog + url = service_catalog.url_for(service_type=service_type, + endpoint_type=interface, + region_name=region_name, + service_name=service_name) + + if not version: + # NOTE(jamielennox): This may not be the best thing to default to + # but is here for backwards compatibility. It may be worth + # defaulting to the most recent version. + return url + + # NOTE(jamielennox): For backwards compatibility people might have a + # versioned endpoint in their catalog even though they want to use + # other endpoint versions. So we support a list of client defined + # situations where we can strip the version component from a URL before + # doing discovery. + hacked_url = _discover.get_catalog_discover_hack(service_type, url) + + try: + disc = self.get_discovery(session, hacked_url, authenticated=False) + except (exceptions.DiscoveryFailure, + exceptions.HTTPError, + exceptions.ConnectionError): + # NOTE(jamielennox): Again if we can't contact the server we fall + # back to just returning the URL from the catalog. This may not be + # the best default but we need it for now. + LOG.warning( + 'Failed to contact the endpoint at %s for discovery. Fallback ' + 'to using that endpoint as the base url.', url) + else: + url = disc.url_for(version) + + return url + + def get_user_id(self, session, **kwargs): + return self.get_access(session).user_id + + def get_project_id(self, session, **kwargs): + return self.get_access(session).project_id + + def get_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: keystoneclient.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 keystoneclient.exceptions.DiscoveryFailure: if for some reason + the lookup fails. + :raises keystoneclient.exceptions.HttpError: An error from an invalid + HTTP response. + + :returns: A discovery object with the results of looking up that URL. + """ + # NOTE(jamielennox): we want to cache endpoints on the session as well + # so that they maintain sharing between auth plugins. Create a cache on + # the session if it doesn't exist already. + try: + session_endpoint_cache = session._identity_endpoint_cache + except AttributeError: + session_endpoint_cache = session._identity_endpoint_cache = {} + + # NOTE(jamielennox): There is a cache located on both the session + # object and the auth plugin object so that they can be shared and the + # cache is still usable + for cache in (self._endpoint_cache, session_endpoint_cache): + disc = cache.get(url) + + if disc: + break + else: + disc = _discover.Discover(session, url, + authenticated=authenticated) + self._endpoint_cache[url] = disc + session_endpoint_cache[url] = disc + + return disc + + @classmethod + def get_options(cls): + options = super(BaseIdentityPlugin, cls).get_options() + options.extend(get_options()) + return options diff --git a/keystoneclient/auth/identity/generic/__init__.py b/keystoneclient/auth/identity/generic/__init__.py new file mode 100644 index 000000000..a96fb9791 --- /dev/null +++ b/keystoneclient/auth/identity/generic/__init__.py @@ -0,0 +1,21 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.auth.identity.generic.base import BaseGenericPlugin # noqa +from keystoneclient.auth.identity.generic.password import Password # noqa +from keystoneclient.auth.identity.generic.token import Token # noqa + + +__all__ = ('BaseGenericPlugin', + 'Password', + 'Token', + ) diff --git a/keystoneclient/auth/identity/generic/base.py b/keystoneclient/auth/identity/generic/base.py new file mode 100644 index 000000000..1cf3e0a9a --- /dev/null +++ b/keystoneclient/auth/identity/generic/base.py @@ -0,0 +1,190 @@ +# 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 logging +import urllib.parse as urlparse + +from oslo_config import cfg + +from keystoneclient import _discover +from keystoneclient.auth.identity import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +def get_options(): + return [ + cfg.StrOpt('domain-id', help='Domain ID to scope to'), + cfg.StrOpt('domain-name', help='Domain name to scope to'), + cfg.StrOpt('tenant-id', help='Tenant ID to scope to'), + cfg.StrOpt('tenant-name', help='Tenant name to scope to'), + cfg.StrOpt('project-id', help='Project ID to scope to'), + cfg.StrOpt('project-name', help='Project name to scope to'), + cfg.StrOpt('project-domain-id', + help='Domain ID containing project'), + cfg.StrOpt('project-domain-name', + help='Domain name containing project'), + cfg.StrOpt('trust-id', help='Trust ID'), + ] + + +class BaseGenericPlugin(base.BaseIdentityPlugin, metaclass=abc.ABCMeta): + """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): + super(BaseGenericPlugin, self).__init__(auth_url=auth_url) + + self._project_id = project_id or tenant_id + self._project_name = project_name or tenant_name + self._project_domain_id = project_domain_id + self._project_domain_name = project_domain_name + self._domain_id = domain_id + self._domain_name = domain_name + self._trust_id = trust_id + + self._plugin = None + + @property + def trust_id(self): + # Override to remove deprecation. + return self._trust_id + + @trust_id.setter + def trust_id(self, value): + # Override to remove deprecation. + self._trust_id = value + + @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: keystoneclient.session.Session + :param tuple version: A tuple of the API version at the URL. + :param string url: The base URL for this version. + :param string raw_status: The status that was in the discovery field. + + :returns: A plugin that can match the parameters or None if nothing. + """ + return None # pragma: no cover + + @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 parameters that are common to v2 plugins.""" + return {'trust_id': self._trust_id, + 'tenant_id': self._project_id, + 'tenant_name': self._project_name} + + @property + def _v3_params(self): + """Return 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} + + def _do_create_plugin(self, session): + plugin = None + + try: + disc = self.get_discovery(session, + self.auth_url, + authenticated=False) + except (exceptions.DiscoveryFailure, + exceptions.HTTPError, + exceptions.ConnectionError): + LOG.warning('Discovering versions from the identity service ' + 'failed when creating the password plugin. ' + 'Attempting to determine version from URL.') + + url_parts = urlparse.urlparse(self.auth_url) + path = url_parts.path.lower() + + if path.startswith('/v2.0') and not self._has_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: + disc_data = disc.version_data() + + 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. + continue + + plugin = self.create_plugin(session, + version, + data['url'], + raw_status=data['raw_status']) + + if plugin: + break + + if plugin: + return plugin + + # so there were no URLs that i could use for auth of any version. + msg = _('Could not determine a suitable URL for the plugin') + raise exceptions.DiscoveryFailure(msg) + + 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) + + @classmethod + def get_options(cls): + options = super(BaseGenericPlugin, cls).get_options() + options.extend(get_options()) + return options diff --git a/keystoneclient/auth/identity/generic/cli.py b/keystoneclient/auth/identity/generic/cli.py new file mode 100644 index 000000000..de1d74895 --- /dev/null +++ b/keystoneclient/auth/identity/generic/cli.py @@ -0,0 +1,82 @@ +# 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 oslo_config import cfg + +from keystoneclient.auth.identity.generic import password +from keystoneclient import exceptions as exc +from keystoneclient.i18n import _ + + +class DefaultCLI(password.Password): + """A Plugin that provides typical authentication options for CLIs. + + This plugin provides standard username and password authentication options + as well as allowing users to override with a custom token and endpoint. + """ + + def __init__(self, endpoint=None, token=None, **kwargs): + super(DefaultCLI, self).__init__(**kwargs) + + self._token = token + self._endpoint = endpoint + + @classmethod + def get_options(cls): + options = super(DefaultCLI, cls).get_options() + options.extend([cfg.StrOpt('endpoint', + help='A URL to use instead of a catalog'), + cfg.StrOpt('token', + secret=True, + help='Always use the specified token')]) + return options + + def get_token(self, *args, **kwargs): + if self._token: + return self._token + + return super(DefaultCLI, self).get_token(*args, **kwargs) + + def get_endpoint(self, *args, **kwargs): + if self._endpoint: + return self._endpoint + + return super(DefaultCLI, self).get_endpoint(*args, **kwargs) + + @classmethod + def load_from_argparse_arguments(cls, namespace, **kwargs): + token = kwargs.get('token') or namespace.os_token + endpoint = kwargs.get('endpoint') or namespace.os_endpoint + auth_url = kwargs.get('auth_url') or namespace.os_auth_url + + if token and not endpoint: + # if a user provides a token then they must also provide an + # endpoint because we aren't fetching a token to get a catalog from + msg = _('A service URL must be provided with a token') + raise exc.CommandError(msg) + elif (not token) and (not auth_url): + # if you don't provide a token you are going to provide at least an + # auth_url with which to authenticate. + raise exc.CommandError(_('Expecting an auth URL via either ' + '--os-auth-url or env[OS_AUTH_URL]')) + + plugin = super(DefaultCLI, cls).load_from_argparse_arguments(namespace, + **kwargs) + + if (not token) and (not plugin._password): + # we do this after the load so that the base plugin has an + # opportunity to prompt the user for a password + raise exc.CommandError(_('Expecting a password provided via ' + 'either --os-password, env[OS_PASSWORD], ' + 'or prompted response')) + + return plugin diff --git a/keystoneclient/auth/identity/generic/password.py b/keystoneclient/auth/identity/generic/password.py new file mode 100644 index 000000000..ddcdba8e3 --- /dev/null +++ b/keystoneclient/auth/identity/generic/password.py @@ -0,0 +1,87 @@ +# 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 oslo_config import cfg + +from keystoneclient import _discover +from keystoneclient.auth.identity.generic import base +from keystoneclient.auth.identity import v2 +from keystoneclient.auth.identity import v3 +from keystoneclient import utils + + +def get_options(): + return [ + cfg.StrOpt('user-id', help='User id'), + cfg.StrOpt('username', dest='username', help='Username', + deprecated_name='user-name'), + cfg.StrOpt('user-domain-id', help="User's domain id"), + cfg.StrOpt('user-domain-name', help="User's domain name"), + cfg.StrOpt('password', secret=True, help="User's password"), + ] + + +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. + + """ + + 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: + # If you specify any domain parameters it won't work so quit. + 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): + return v3.Password(auth_url=url, + user_id=self._user_id, + username=self._username, + user_domain_id=self._user_domain_id, + user_domain_name=self._user_domain_name, + password=self._password, + **self._v3_params) + + @classmethod + def get_options(cls): + options = super(Password, cls).get_options() + options.extend(get_options()) + return options + + @classmethod + def load_from_argparse_arguments(cls, namespace, **kwargs): + if not (kwargs.get('password') or namespace.os_password): + kwargs['password'] = utils.prompt_user_password() + + return super(Password, cls).load_from_argparse_arguments(namespace, + **kwargs) diff --git a/keystoneclient/auth/identity/generic/token.py b/keystoneclient/auth/identity/generic/token.py new file mode 100644 index 000000000..e3d01aa0f --- /dev/null +++ b/keystoneclient/auth/identity/generic/token.py @@ -0,0 +1,48 @@ +# 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 oslo_config import cfg + +from keystoneclient import _discover +from keystoneclient.auth.identity.generic import base +from keystoneclient.auth.identity import v2 +from keystoneclient.auth.identity import v3 + + +def get_options(): + return [ + cfg.StrOpt('token', secret=True, help='Token to authenticate with'), + ] + + +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) + + @classmethod + def get_options(cls): + options = super(Token, cls).get_options() + options.extend(get_options()) + return options diff --git a/keystoneclient/auth/identity/v2.py b/keystoneclient/auth/identity/v2.py new file mode 100644 index 000000000..b2ecb4b59 --- /dev/null +++ b/keystoneclient/auth/identity/v2.py @@ -0,0 +1,237 @@ +# 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 logging + +from oslo_config import cfg + +from keystoneclient import access +from keystoneclient.auth.identity import base +from keystoneclient import exceptions +from keystoneclient import utils + +_logger = logging.getLogger(__name__) + + +class Auth(base.BaseIdentityPlugin, metaclass=abc.ABCMeta): + """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 + """ + + @classmethod + def get_options(cls): + options = super(Auth, cls).get_options() + + options.extend([ + cfg.StrOpt('tenant-id', help='Tenant ID'), + cfg.StrOpt('tenant-name', help='Tenant Name'), + cfg.StrOpt('trust-id', help='Trust ID'), + ]) + + return options + + 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 + + @property + def trust_id(self): + # Override to remove deprecation. + return self._trust_id + + @trust_id.setter + def trust_id(self, value): + # Override to remove deprecation. + self._trust_id = value + + 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()['access'] + except (KeyError, ValueError): + 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 + """ + pass # pragma: no cover + + +_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. + """ + + 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 + + @property + def username(self): + # Override to remove deprecation. + return self._username + + @username.setter + def username(self, value): + # Override to remove deprecation. + self._username = value + + @property + def password(self): + # Override to remove deprecation. + return self._password + + @password.setter + def password(self, value): + # Override to remove deprecation. + self._password = value + + 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} + + @classmethod + def load_from_argparse_arguments(cls, namespace, **kwargs): + if not (kwargs.get('password') or namespace.os_password): + kwargs['password'] = utils.prompt_user_password() + + return super(Password, cls).load_from_argparse_arguments(namespace, + **kwargs) + + @classmethod + def get_options(cls): + options = super(Password, cls).get_options() + + options.extend([ + cfg.StrOpt('username', + dest='username', + deprecated_name='user-name', + help='Username to login with'), + cfg.StrOpt('user-id', help='User ID to login with'), + cfg.StrOpt('password', secret=True, help='Password to use'), + ]) + + return options + + +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 + + @property + def token(self): + # Override to remove deprecation. + return self._token + + @token.setter + def token(self, value): + # Override to remove deprecation. + self._token = value + + def get_auth_data(self, headers=None): + if headers is not None: + headers['X-Auth-Token'] = self.token + return {'token': {'id': self.token}} + + @classmethod + def get_options(cls): + options = super(Token, cls).get_options() + + options.extend([ + cfg.StrOpt('token', secret=True, help='Token'), + ]) + + return options diff --git a/keystoneclient/auth/identity/v3/__init__.py b/keystoneclient/auth/identity/v3/__init__.py new file mode 100644 index 000000000..abbaa658d --- /dev/null +++ b/keystoneclient/auth/identity/v3/__init__.py @@ -0,0 +1,32 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# flake8: noqa: F405 + +from keystoneclient.auth.identity.v3.base import * # noqa +from keystoneclient.auth.identity.v3.federated import * # noqa +from keystoneclient.auth.identity.v3.password import * # noqa +from keystoneclient.auth.identity.v3.token import * # noqa + + +__all__ = ('Auth', + 'AuthConstructor', + 'AuthMethod', + 'BaseAuth', + + 'FederatedBaseAuth', + + 'Password', + 'PasswordMethod', + + 'Token', + 'TokenMethod') diff --git a/keystoneclient/auth/identity/v3/base.py b/keystoneclient/auth/identity/v3/base.py new file mode 100644 index 000000000..c055d4ff7 --- /dev/null +++ b/keystoneclient/auth/identity/v3/base.py @@ -0,0 +1,259 @@ +# 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 logging + +from oslo_config import cfg +from oslo_serialization import jsonutils + +from keystoneclient import access +from keystoneclient.auth.identity import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ + +_logger = logging.getLogger(__name__) + +__all__ = ('Auth', 'AuthMethod', 'AuthConstructor', 'BaseAuth') + + +class BaseAuth(base.BaseIdentityPlugin, metaclass=abc.ABCMeta): + """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. + """ + + 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 trust_id(self): + # Override to remove deprecation. + return self._trust_id + + @trust_id.setter + def trust_id(self, value): + # Override to remove deprecation. + self._trust_id = value + + @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 # pragma: no cover + + @classmethod + def get_options(cls): + options = super(BaseAuth, cls).get_options() + + options.extend([ + cfg.StrOpt('domain-id', help='Domain ID to scope to'), + cfg.StrOpt('domain-name', help='Domain name to scope to'), + cfg.StrOpt('project-id', help='Project ID to scope to'), + cfg.StrOpt('project-name', help='Project name to scope to'), + cfg.StrOpt('project-domain-id', + help='Domain ID containing project'), + cfg.StrOpt('project-domain-name', + help='Domain name containing project'), + cfg.StrOpt('trust-id', help='Trust ID'), + ]) + + return options + + +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(jsonutils.dumps(resp.json())) + resp_data = resp.json()['token'] + except (KeyError, ValueError): + raise exceptions.InvalidResponse(response=resp) + + return access.AccessInfoV3(resp.headers['X-Subject-Token'], + **resp_data) + + +class AuthMethod(object, metaclass=abc.ABCMeta): + """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) + 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: keystoneclient.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) + """ + pass # pragma: no cover + + +class AuthConstructor(Auth, metaclass=abc.ABCMeta): + """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) diff --git a/keystoneclient/auth/identity/v3/federated.py b/keystoneclient/auth/identity/v3/federated.py new file mode 100644 index 000000000..755e7f51b --- /dev/null +++ b/keystoneclient/auth/identity/v3/federated.py @@ -0,0 +1,119 @@ +# 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 oslo_config import cfg + +from keystoneclient.auth.identity.v3 import base +from keystoneclient.auth.identity.v3 import token + +__all__ = ('FederatedBaseAuth',) + + +class FederatedBaseAuth(base.BaseAuth, metaclass=abc.ABCMeta): + + rescoping_plugin = token.Token + + def __init__(self, auth_url, identity_provider, protocol, **kwargs): + """Class constructor for federated authentication plugins. + + Accepting following parameters: + + :param auth_url: URL of the Identity Service + :type auth_url: string + :param identity_provider: Name of the Identity Provider the client + will authenticate against. This parameter + will be used to build a dynamic URL used to + obtain unscoped OpenStack token. + :type identity_provider: string + :param protocol: Protocol name configured on the keystone service + provider side + :type protocol: string + + """ + super(FederatedBaseAuth, self).__init__(auth_url=auth_url, **kwargs) + self.identity_provider = identity_provider + self.protocol = protocol + + @classmethod + def get_options(cls): + options = super(FederatedBaseAuth, cls).get_options() + + options.extend([ + cfg.StrOpt('identity-provider', + help="Identity Provider's name"), + cfg.StrOpt('protocol', help="Name of the federated protocol used " + "for federated authentication. Must " + "match its counterpart name " + "configured at the keystone service " + "provider. Typically values would be " + "'saml2' or 'oidc'.") + ]) + + return options + + @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 + + 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: keystoneclient.session.Session + + :returns: a token data representation + :rtype: :py:class:`keystoneclient.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.""" + pass # pragma: no cover diff --git a/keystoneclient/auth/identity/v3/password.py b/keystoneclient/auth/identity/v3/password.py new file mode 100644 index 000000000..99094a1e2 --- /dev/null +++ b/keystoneclient/auth/identity/v3/password.py @@ -0,0 +1,97 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg + +from keystoneclient.auth.identity.v3 import base +from keystoneclient import utils + + +__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} + + +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 + + @classmethod + def get_options(cls): + options = super(Password, cls).get_options() + + options.extend([ + cfg.StrOpt('user-id', help='User ID'), + cfg.StrOpt('username', dest='username', help='Username', + deprecated_name='user-name'), + cfg.StrOpt('user-domain-id', help="User's domain id"), + cfg.StrOpt('user-domain-name', help="User's domain name"), + cfg.StrOpt('password', secret=True, help="User's password"), + ]) + + return options + + @classmethod + def load_from_argparse_arguments(cls, namespace, **kwargs): + if not (kwargs.get('password') or namespace.os_password): + kwargs['password'] = utils.prompt_user_password() + + return super(Password, cls).load_from_argparse_arguments(namespace, + **kwargs) diff --git a/keystoneclient/auth/identity/v3/token.py b/keystoneclient/auth/identity/v3/token.py new file mode 100644 index 000000000..396a11a20 --- /dev/null +++ b/keystoneclient/auth/identity/v3/token.py @@ -0,0 +1,65 @@ +# 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 oslo_config import cfg + +from keystoneclient.auth.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} + + +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) + + @classmethod + def get_options(cls): + options = super(Token, cls).get_options() + + options.extend([ + cfg.StrOpt('token', + secret=True, + help='Token to authenticate with'), + ]) + + return options diff --git a/keystoneclient/auth/token_endpoint.py b/keystoneclient/auth/token_endpoint.py new file mode 100644 index 000000000..6b60f9cb8 --- /dev/null +++ b/keystoneclient/auth/token_endpoint.py @@ -0,0 +1,62 @@ +# 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 warnings + +from oslo_config import cfg + +from keystoneclient.auth import base + + +class Token(base.BaseAuthPlugin): + """A provider that will always use the given token and endpoint. + + This is really only useful for testing and in certain CLI cases where you + have a known endpoint and admin token that you want to use. + """ + + def __init__(self, endpoint, token): + # NOTE(jamielennox): endpoint is reserved for when plugins + # can be used to provide that information + warnings.warn( + 'TokenEndpoint plugin is deprecated as of the 2.1.0 release in ' + 'favor of keystoneauth1.token_endpoint.Token. It will be removed ' + 'in future releases.', + DeprecationWarning) + + self.endpoint = endpoint + self.token = token + + def get_token(self, session): + return self.token + + def get_endpoint(self, session, **kwargs): + """Return the supplied endpoint. + + Using this plugin the same endpoint is returned regardless of the + parameters passed to the plugin. + """ + return self.endpoint + + @classmethod + def get_options(cls): + options = super(Token, cls).get_options() + + options.extend([ + cfg.StrOpt('endpoint', + help='The endpoint that will always be used'), + cfg.StrOpt('token', + secret=True, + help='The token that will always be used'), + ]) + + return options diff --git a/keystoneclient/base.py b/keystoneclient/base.py index fd9e80205..4f3ed227a 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -1,5 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation +# Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,57 +15,129 @@ # License for the specific language governing permissions and limitations # under the License. -""" -Base utilities to build API operation managers and objects on top of. -""" +"""Base utilities to build API operation managers and objects on top of.""" +import abc +import copy +import functools import urllib +import warnings -from keystoneclient import exceptions +from keystoneauth1 import exceptions as ksa_exceptions +from keystoneauth1 import plugin +from oslo_utils import strutils +from keystoneclient import exceptions as ksc_exceptions +from keystoneclient.i18n import _ -# Python 2.4 compat -try: - all -except NameError: - def all(iterable): - return True not in (not x for x in iterable) + +class Response(object): + + def __init__(self, http_response, data): + self.request_ids = [] + if isinstance(http_response, list): + # http_response is a list of in case + # of pagination + for resp_obj in http_response: + # Extract 'x-openstack-request-id' from headers + self.request_ids.append(resp_obj.headers.get( + 'x-openstack-request-id')) + else: + self.request_ids.append(http_response.headers.get( + 'x-openstack-request-id')) + self.data = data def getid(obj): - """ + """Return id if argument is a Resource. + Abstracts the common pattern of allowing both an object or an object's ID (UUID) as a parameter when dealing with relationships. """ + if getattr(obj, 'uuid', None): + return obj.uuid + else: + return getattr(obj, 'id', obj) + + +def filter_none(**kwargs): + """Remove any entries from a dictionary where the value is None.""" + return dict((k, v) for k, v in kwargs.items() if v is not None) + + +def filter_kwargs(f): + @functools.wraps(f) + def func(*args, **kwargs): + new_kwargs = {} + for key, ref in kwargs.items(): + if ref is None: + # drop null values + continue + + id_value = getid(ref) + if id_value != ref: + # If an object with an id was passed, then use the id, e.g.: + # user: user(id=1) becomes user_id: 1 + key = '%s_id' % key - # Try to return the object's UUID first, if we have a UUID. - try: - if obj.uuid: - return obj.uuid - except AttributeError: - pass - try: - return obj.id - except AttributeError: - return obj + new_kwargs[key] = id_value + + return f(*args, **new_kwargs) + return func class Manager(object): - """ + """Basic manager type providing common operations. + Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. + + :param client: instance of BaseClient descendant for HTTP requests + """ + resource_class = None - def __init__(self, api): - self.api = api + def __init__(self, client): + super(Manager, self).__init__() + self.client = client - def _list(self, url, response_key, obj_class=None, body=None): - resp = None + @property + def api(self): + """The client. + + .. warning:: + + This property is deprecated as of the 1.7.0 release in favor of + :meth:`client` and may be removed in the 2.0.0 release. + + """ + warnings.warn( + 'api is deprecated as of the 1.7.0 release in favor of client and ' + 'may be removed in the 2.0.0 release', DeprecationWarning) + return self.client + + def _prepare_return_value(self, http_response, data): + if self.client.include_metadata: + return Response(http_response, data) + return data + + def _list(self, url, response_key, obj_class=None, body=None, **kwargs): + """List the collection. + + :param url: a partial URL, e.g., '/servers' + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + :param obj_class: class for constructing the returned objects + (self.resource_class will be used by default) + :param body: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param kwargs: Additional arguments will be passed to the request. + """ if body: - resp, body = self.api.post(url, body=body) + resp, body = self.client.post(url, body=body, **kwargs) else: - resp, body = self.api.get(url) + resp, body = self.client.get(url, **kwargs) if obj_class is None: obj_class = self.resource_class @@ -72,67 +145,156 @@ def _list(self, url, response_key, obj_class=None, body=None): data = body[response_key] # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... - if type(data) is dict: + try: data = data['values'] - return [obj_class(self, res, loaded=True) for res in data if res] + except (KeyError, TypeError): # nosec(cjschaef): keystone data values + # not as expected (see comment above), assumption is that values + # are already returned in a list (so simply utilize that list) + pass - def _get(self, url, response_key): - resp, body = self.api.get(url) - return self.resource_class(self, body[response_key], loaded=True) + return self._prepare_return_value( + resp, [obj_class(self, res, loaded=True) for res in data if res]) - def _head(self, url): - resp, body = self.api.head(url) - return resp.status_code == 204 + def _get(self, url, response_key, **kwargs): + """Get an object from collection. - def _create(self, url, body, response_key, return_raw=False): - resp, body = self.api.post(url, body=body) + :param url: a partial URL, e.g., '/servers' + :param response_key: the key to be looked up in response dictionary, + e.g., 'server' + :param kwargs: Additional arguments will be passed to the request. + """ + resp, body = self.client.get(url, **kwargs) + return self._prepare_return_value( + resp, self.resource_class(self, body[response_key], loaded=True)) + + def _head(self, url, **kwargs): + """Retrieve request headers for an object. + + :param url: a partial URL, e.g., '/servers' + :param kwargs: Additional arguments will be passed to the request. + """ + resp, body = self.client.head(url, **kwargs) + return self._prepare_return_value(resp, resp.status_code == 204) + + def _post(self, url, body, response_key, return_raw=False, **kwargs): + """Create an object. + + :param url: a partial URL, e.g., '/servers' + :param body: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + :param return_raw: flag to force returning raw JSON instead of + Python object of self.resource_class + :param kwargs: Additional arguments will be passed to the request. + """ + resp, body = self.client.post(url, body=body, **kwargs) if return_raw: return body[response_key] - return self.resource_class(self, body[response_key]) + return self._prepare_return_value( + resp, self.resource_class(self, body[response_key])) + + def _put(self, url, body=None, response_key=None, **kwargs): + """Update an object with PUT method. + + :param url: a partial URL, e.g., '/servers' + :param body: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + :param kwargs: Additional arguments will be passed to the request. + """ + resp, body = self.client.put(url, body=body, **kwargs) + # PUT requests may not return a body + if body is not None: + if response_key is not None: + return self._prepare_return_value( + resp, self.resource_class(self, body[response_key])) + else: + return self._prepare_return_value( + resp, self.resource_class(self, body)) + # In some cases (e.g. 'add_endpoint_to_project' from endpoint_filters + # resource), PUT request may not return a body so return None as + # response along with request_id if include_metadata is True. + return self._prepare_return_value(resp, body) + + def _patch(self, url, body=None, response_key=None, **kwargs): + """Update an object with PATCH method. + + :param url: a partial URL, e.g., '/servers' + :param body: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + :param kwargs: Additional arguments will be passed to the request. + """ + resp, body = self.client.patch(url, body=body, **kwargs) + if response_key is not None: + return self._prepare_return_value( + resp, self.resource_class(self, body[response_key])) + else: + return self._prepare_return_value( + resp, self.resource_class(self, body)) + + def _delete(self, url, **kwargs): + """Delete an object. - def _delete(self, url): - resp, body = self.api.delete(url) + :param url: a partial URL, e.g., '/servers/my-server' + :param kwargs: Additional arguments will be passed to the request. + """ + resp, body = self.client.delete(url, **kwargs) + return resp, self._prepare_return_value(resp, body) def _update(self, url, body=None, response_key=None, method="PUT", - management=True): - methods = {"PUT": self.api.put, - "POST": self.api.post, - "PATCH": self.api.patch} + **kwargs): + methods = {"PUT": self.client.put, + "POST": self.client.post, + "PATCH": self.client.patch} try: - if body is not None: - resp, body = methods[method](url, body=body, - management=management) - else: - resp, body = methods[method](url, management=management) + resp, body = methods[method](url, body=body, + **kwargs) except KeyError: - raise exceptions.ClientException("Invalid update method: %s" - % method) + raise ksc_exceptions.ClientException(_("Invalid update method: %s") + % method) # PUT requests may not return a body if body: - return self.resource_class(self, body[response_key]) + return self._prepare_return_value( + resp, self.resource_class(self, body[response_key])) + else: + return self._prepare_return_value(resp, body) -class ManagerWithFind(Manager): - """ - Like a `Manager`, but with additional `find()`/`findall()` methods. - """ +class ManagerWithFind(Manager, metaclass=abc.ABCMeta): + """Manager with additional `find()`/`findall()` methods.""" + + @abc.abstractmethod + def list(self): + pass # pragma: no cover + def find(self, **kwargs): - """ - Find a single item with attributes matching ``**kwargs``. + """Find a single item with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ rl = self.findall(**kwargs) - try: - return rl[0] - except IndexError: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) - raise exceptions.NotFound(404, msg) + + if self.client.include_metadata: + base_response = rl + rl = rl.data + base_response.data = rl[0] + + if len(rl) == 0: + msg = _("No %(name)s matching %(kwargs)s.") % { + 'name': self.resource_class.__name__, 'kwargs': kwargs} + raise ksa_exceptions.NotFound(404, msg) + elif len(rl) > 1: + raise ksc_exceptions.NoUniqueMatch + else: + return base_response if self.client.include_metadata else rl[0] def findall(self, **kwargs): - """ - Find all items with attributes matching ``**kwargs``. + """Find all items with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. @@ -140,15 +302,23 @@ def findall(self, **kwargs): found = [] searches = kwargs.items() - for obj in self.list(): - try: - if all(getattr(obj, attr) == value - for (attr, value) in searches): - found.append(obj) - except AttributeError: - continue + def _extract_data(objs, response_data): + for obj in objs: + try: + if all(getattr(obj, attr) == value + for (attr, value) in searches): + response_data.append(obj) + except AttributeError: + continue + return response_data + + objs = self.list() + if self.client.include_metadata: + # 'objs' is the object of 'Response' class. + objs.data = _extract_data(objs.data, found) + return objs - return found + return _extract_data(objs, found) class CrudManager(Manager): @@ -164,11 +334,13 @@ class CrudManager(Manager): refer to an individual member of the collection. """ + collection_key = None key = None + base_url = None - def build_url(self, base_url=None, **kwargs): - """Builds a resource URL for the given kwargs. + def build_url(self, dict_args_in_out=None): + """Build a resource URL for the given kwargs. Given an example collection where `collection_key = 'entities'` and `key = 'entity'`, the following URL's could be generated. @@ -184,104 +356,215 @@ def build_url(self, base_url=None, **kwargs): If a `base_url` is provided, the generated URL will be appended to it. + If a 'tail' is provided, it will be appended to the end of the URL. + """ - url = base_url if base_url is not None else '' + if dict_args_in_out is None: + dict_args_in_out = {} + url = dict_args_in_out.pop('base_url', None) or self.base_url or '' url += '/%s' % self.collection_key # do we have a specific entity? - entity_id = kwargs.get('%s_id' % self.key) + entity_id = dict_args_in_out.pop('%s_id' % self.key, None) if entity_id is not None: url += '/%s' % entity_id - return url + if dict_args_in_out.get('tail'): + url += dict_args_in_out['tail'] - def _filter_kwargs(self, kwargs): - # drop null values - for key, ref in kwargs.copy().iteritems(): - if ref is None: - kwargs.pop(key) - else: - id_value = getid(ref) - if id_value != ref: - kwargs.pop(key) - kwargs['%s_id' % key] = id_value - return kwargs + return url + @filter_kwargs def create(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._create( - self.build_url(**kwargs), + url = self.build_url(dict_args_in_out=kwargs) + return self._post( + url, {self.key: kwargs}, self.key) + @filter_kwargs def get(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) return self._get( - self.build_url(**kwargs), + self.build_url(dict_args_in_out=kwargs), self.key) + @filter_kwargs def head(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._head(self.build_url(**kwargs)) + return self._head(self.build_url(dict_args_in_out=kwargs)) - def list(self, base_url=None, **kwargs): - kwargs = self._filter_kwargs(kwargs) - - return self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) + def _build_query(self, params): + if params is None: + return '' + else: + # NOTE(spilla) Since the manager cannot take in a hyphen as a + # key in the kwarg, it is passed in with a _. This needs to be + # replaced with a proper hyphen for the URL to work properly. + tags_params = ('tags_any', 'not_tags', 'not_tags_any') + for tag_param in tags_params: + if tag_param in params: + params[tag_param.replace('_', '-')] = params.pop(tag_param) + return '?%s' % urllib.parse.urlencode(params, doseq=True) + + def build_key_only_query(self, params_list): + """Build a query that does not include values, just keys. + + The Identity API has some calls that define queries without values, + this can not be accomplished by using urllib.parse.urlencode(). This + method builds a query using only the keys. + """ + return '?%s' % '&'.join(params_list) if params_list else '' + + @filter_kwargs + def list(self, fallback_to_auth=False, **kwargs): + + def return_resp(resp, include_metadata=False): + base_response = None + list_data = resp + if include_metadata: + base_response = resp + list_data = resp.data + base_response.data = list_data + return base_response if include_metadata else list_data + + if 'id' in kwargs.keys(): + # Ensure that users are not trying to call things like + # ``domains.list(id='default')`` when they should have used + # ``[domains.get(domain_id='default')]`` instead. Keystone supports + # ``GET /v3/domains/{domain_id}``, not ``GET + # /v3/domains?id={domain_id}``. + raise TypeError( + _("list() got an unexpected keyword argument 'id'. To " + "retrieve a single object using a globally unique " + "identifier, try using get() instead.")) + + url = self.build_url(dict_args_in_out=kwargs) - def put(self, base_url=None, **kwargs): - kwargs = self._filter_kwargs(kwargs) + try: + query = self._build_query(kwargs) + url_query = '%(url)s%(query)s' % {'url': url, 'query': query} + list_resp = self._list(url_query, self.collection_key) + return return_resp(list_resp, + include_metadata=self.client.include_metadata) + except ksa_exceptions.EmptyCatalog: + if fallback_to_auth: + list_resp = self._list(url_query, self.collection_key, + endpoint_filter={ + 'interface': plugin.AUTH_INTERFACE}) + return return_resp( + list_resp, include_metadata=self.client.include_metadata) + else: + raise + @filter_kwargs + def put(self, **kwargs): return self._update( - self.build_url(base_url=base_url, **kwargs), + self.build_url(dict_args_in_out=kwargs), method='PUT') + @filter_kwargs def update(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - params = kwargs.copy() - params.pop('%s_id' % self.key) + url = self.build_url(dict_args_in_out=kwargs) return self._update( - self.build_url(**kwargs), - {self.key: params}, + url, + {self.key: kwargs}, self.key, method='PATCH') + @filter_kwargs def delete(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._delete( - self.build_url(**kwargs)) + self.build_url(dict_args_in_out=kwargs)) + + @filter_kwargs + def find(self, **kwargs): + """Find a single item with attributes matching ``**kwargs``.""" + url = self.build_url(dict_args_in_out=kwargs) + + query = self._build_query(kwargs) + url_query = '%(url)s%(query)s' % { + 'url': url, + 'query': query + } + elements = self._list( + url_query, + self.collection_key) + + if self.client.include_metadata: + base_response = elements + elements = elements.data + base_response.data = elements[0] + + if not elements: + msg = _("No %(name)s matching %(kwargs)s.") % { + 'name': self.resource_class.__name__, 'kwargs': kwargs} + raise ksa_exceptions.NotFound(404, msg) + elif len(elements) > 1: + raise ksc_exceptions.NoUniqueMatch + else: + return (base_response if self.client.include_metadata + else elements[0]) class Resource(object): - """ - A resource represents a particular instance of an object (tenant, user, - etc). This is pretty much just a bag for attributes. + """Base class for OpenStack resources (tenant, user, etc.). - :param manager: Manager object - :param info: dictionary representing resource attributes - :param loaded: prevent lazy-loading if set to True + This is pretty much just a bag for attributes. """ + + HUMAN_ID = False + NAME_ATTR = 'name' + def __init__(self, manager, info, loaded=False): + """Populate and bind to a manager. + + :param manager: BaseManager object + :param info: dictionary representing resource attributes + :param loaded: prevent lazy-loading if set to True + """ self.manager = manager self._info = info self._add_details(info) self._loaded = loaded + def __repr__(self): + """Return string representation of resource attributes.""" + reprkeys = sorted(k + for k in self.__dict__.keys() + if k[0] != '_' and k != 'manager') + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + @property + def human_id(self): + """Human-readable ID which can be used for bash completion.""" + if self.HUMAN_ID: + name = getattr(self, self.NAME_ATTR, None) + if name is not None: + return strutils.to_slug(name) + return None + def _add_details(self, info): - for (k, v) in info.iteritems(): - setattr(self, k, v) + for (k, v) in info.items(): + try: + try: + setattr(self, k, v) + except UnicodeEncodeError: + # This happens when we're running with Python version that + # does not support Unicode identifiers (e.g. Python 2.7). + # In that case we can't help but not set this attribute; + # it'll be available in a dict representation though + pass + self._info[k] = v + except AttributeError: # nosec(cjschaef): we already defined the + # attribute on the class + pass def __getattr__(self, k): + """Checking attribute existence.""" if k not in self.__dict__: - #NOTE(bcwaldon): disallow lazy-loading if already loaded once + # NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) @@ -290,13 +573,12 @@ def __getattr__(self, k): else: return self.__dict__[k] - def __repr__(self): - reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and - k != 'manager') - info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) - def get(self): + """Support for lazy loading details. + + Some clients, such as novaclient have the option to lazy load the + details, details which can be loaded with this function. + """ # set_loaded() first ... so if we have to bail, we know we tried. self.set_loaded(True) if not hasattr(self.manager, 'get'): @@ -306,18 +588,27 @@ def get(self): if new: self._add_details(new._info) - def delete(self): - return self.manager.delete(self) - def __eq__(self, other): + """Define equality for resources.""" + if not isinstance(other, Resource): + return NotImplemented + # two resources of different types are not equal if not isinstance(other, self.__class__): return False - if hasattr(self, 'id') and hasattr(other, 'id'): - return self.id == other.id return self._info == other._info + def __ne__(self, other): + """Define inequality for resources.""" + return not self == other + def is_loaded(self): return self._loaded def set_loaded(self, val): self._loaded = val + + def to_dict(self): + return copy.deepcopy(self._info) + + def delete(self): + return self.manager.delete(self) diff --git a/keystoneclient/baseclient.py b/keystoneclient/baseclient.py new file mode 100644 index 000000000..ca39f6d14 --- /dev/null +++ b/keystoneclient/baseclient.py @@ -0,0 +1,46 @@ +# 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 warnings + + +class Client(object): + + def __init__(self, session): + warnings.warn( + 'keystoneclient.baseclient.Client is deprecated as of the 2.1.0 ' + 'release. It will be removed in future releases.', + DeprecationWarning) + + self.session = session + + def request(self, url, method, **kwargs): + kwargs.setdefault('authenticated', True) + return self.session.request(url, method, **kwargs) + + 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) diff --git a/keystoneclient/client.py b/keystoneclient/client.py index 6f2d1a452..b2dcf0628 100644 --- a/keystoneclient/client.py +++ b/keystoneclient/client.py @@ -1,395 +1,48 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. -# Copyright 2011 Piston Cloud Computing, Inc. -# Copyright 2011 Nebula, Inc. - -# All Rights Reserved. -""" -OpenStack Client interface. Handles the REST calls and responses. -""" - -import copy -import logging -import sys -import urlparse - -import requests - -try: - import json -except ImportError: - import simplejson as json - -# Python 2.5 compat fix -if not hasattr(urlparse, 'parse_qsl'): - import cgi - urlparse.parse_qsl = cgi.parse_qsl - - -from keystoneclient import access -from keystoneclient import exceptions - - -_logger = logging.getLogger(__name__) - - -# keyring init -keyring_available = True -try: - import keyring - import pickle -except ImportError: - if (hasattr(sys.stderr, 'isatty') and sys.stderr.isatty()): - print >> sys.stderr, 'Failed to load keyring modules.' - else: - _logger.warning('Failed to load keyring modules.') - keyring_available = False - - -class HTTPClient(object): - - USER_AGENT = 'python-keystoneclient' - - requests_config = { - 'danger_mode': False, - } - - def __init__(self, username=None, tenant_id=None, tenant_name=None, - password=None, auth_url=None, region_name=None, timeout=None, - endpoint=None, token=None, cacert=None, key=None, - cert=None, insecure=False, original_ip=None, debug=False, - auth_ref=None, use_keyring=False, force_new_token=False, - stale_duration=None): - self.version = 'v2.0' - # set baseline defaults - self.username = None - self.tenant_id = None - self.tenant_name = None - self.auth_url = None - self.token = None - self.auth_token = None - self.management_url = None - # if loading from a dictionary passed in via auth_ref, - # load values from AccessInfo parsing that dictionary - self.auth_ref = access.AccessInfo(**auth_ref) if auth_ref else None - if self.auth_ref: - self.username = self.auth_ref.username - self.tenant_id = self.auth_ref.tenant_id - self.tenant_name = self.auth_ref.tenant_name - self.auth_url = self.auth_ref.auth_url[0] - self.management_url = self.auth_ref.management_url[0] - self.auth_token = self.auth_ref.auth_token - # allow override of the auth_ref defaults from explicit - # values provided to the client - if username: - self.username = username - if tenant_id: - self.tenant_id = tenant_id - if tenant_name: - self.tenant_name = tenant_name - if auth_url: - self.auth_url = auth_url.rstrip('/') - if token: - self.auth_token = token - if endpoint: - self.management_url = endpoint.rstrip('/') - self.password = password - self.original_ip = original_ip - self.region_name = region_name - if cacert: - self.verify_cert = cacert - else: - self.verify_cert = True - if insecure: - self.verify_cert = False - self.cert = cert - if cert and key: - self.cert = (cert, key,) - self.domain = '' - - # logging setup - self.debug_log = debug - if self.debug_log: - ch = logging.StreamHandler() - _logger.setLevel(logging.DEBUG) - _logger.addHandler(ch) - self.requests_config['verbose'] = sys.stderr - - # keyring setup - self.use_keyring = use_keyring and keyring_available - self.force_new_token = force_new_token - self.stale_duration = stale_duration or access.STALE_TOKEN_DURATION - self.stale_duration = int(self.stale_duration) - - def authenticate(self, username=None, password=None, tenant_name=None, - tenant_id=None, auth_url=None, token=None): - """ Authenticate user. - - Uses the data provided at instantiation to authenticate against - the Keystone server. This may use either a username and password - or token for authentication. If a tenant name or id was provided - then the resulting authenticated client will be scoped to that - tenant and contain a service catalog of available endpoints. - - With the v2.0 API, if a tenant name or ID is not provided, the - authenication token returned will be 'unscoped' and limited in - capabilities until a fully-scoped token is acquired. - - If successful, sets the self.auth_ref and self.auth_token with - the returned token. If not already set, will also set - self.management_url from the details provided in the token. - - :returns: ``True`` if authentication was successful. - :raises: AuthorizationFailure if unable to authenticate or validate - the existing authorization token - :raises: ValueError if insufficient parameters are used. - - If keyring is used, token is retrieved from keyring instead. - Authentication will only be necessary if any of the following - conditions are met: - - * keyring is not used - * if token is not found in keyring - * if token retrieved from keyring is expired or about to - expired (as determined by stale_duration) - * if force_new_token is true - - """ - auth_url = auth_url or self.auth_url - username = username or self.username - password = password or self.password - tenant_name = tenant_name or self.tenant_name - tenant_id = tenant_id or self.tenant_id - token = token or self.auth_token - - (keyring_key, auth_ref) = self.get_auth_ref_from_keyring(auth_url, - username, - tenant_name, - tenant_id, - token) - new_token_needed = False - if auth_ref is None or self.force_new_token: - new_token_needed = True - raw_token = self.get_raw_token_from_identity_service(auth_url, - username, - password, - tenant_name, - tenant_id, - token) - self.auth_ref = access.AccessInfo(**raw_token) - else: - self.auth_ref = auth_ref - self.process_token() - if new_token_needed: - self.store_auth_ref_into_keyring(keyring_key) - return True - - def _build_keyring_key(self, auth_url, username, tenant_name, - tenant_id, token): - """ Create a unique key for keyring. - - Used to store and retrieve auth_ref from keyring. - - """ - keys = [auth_url, username, tenant_name, tenant_id, token] - for index, key in enumerate(keys): - if key is None: - keys[index] = '?' - keyring_key = '/'.join(keys) - return keyring_key - - def get_auth_ref_from_keyring(self, auth_url, username, tenant_name, - tenant_id, token): - """ Retrieve auth_ref from keyring. - - If auth_ref is found in keyring, (keyring_key, auth_ref) is returned. - Otherwise, (keyring_key, None) is returned. - - :returns: (keyring_key, auth_ref) or (keyring_key, None) - - """ - keyring_key = None - auth_ref = None - if self.use_keyring: - keyring_key = self._build_keyring_key(auth_url, username, - tenant_name, tenant_id, - token) - try: - auth_ref = keyring.get_password("keystoneclient_auth", - keyring_key) - if auth_ref: - auth_ref = pickle.loads(auth_ref) - if auth_ref.will_expire_soon(self.stale_duration): - # token has expired, don't use it - auth_ref = None - except Exception as e: - auth_ref = None - _logger.warning('Unable to retrieve token from keyring %s' % ( - e)) - return (keyring_key, auth_ref) - - def store_auth_ref_into_keyring(self, keyring_key): - """ Store auth_ref into keyring. - - """ - if self.use_keyring: - try: - keyring.set_password("keystoneclient_auth", - keyring_key, - pickle.dumps(self.auth_ref)) - except Exception as e: - _logger.warning("Failed to store token into keyring %s" % (e)) - - def process_token(self): - """ Extract and process information from the new auth_ref. - - """ - raise NotImplementedError - - def get_raw_token_from_identity_service(self, auth_url, username=None, - password=None, tenant_name=None, - tenant_id=None, token=None): - """ Authenticate against the Identity API and get a token. - - Not implemented here because auth protocols should be API - version-specific. - - Expected to authenticate or validate an existing authentication - reference already associated with the client. Invoking this call - *always* makes a call to the Keystone. - - :returns: ``raw token`` - - """ - raise NotImplementedError - - def _extract_service_catalog(self, url, body): - """ Set the client's service catalog from the response data. - - Not implemented here because data returned may be API - version-specific. - """ - raise NotImplementedError - - def http_log_req(self, args, kwargs): - if not self.debug_log: - return - - string_parts = ['curl -i'] - for element in args: - if element in ('GET', 'POST'): - string_parts.append(' -X %s' % element) - else: - string_parts.append(' %s' % element) - - for element in kwargs['headers']: - header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) - string_parts.append(header) - - _logger.debug("REQ: %s" % "".join(string_parts)) - if 'body' in kwargs: - _logger.debug("REQ BODY: %s\n" % (kwargs['body'])) - - def http_log_resp(self, resp): - if self.debug_log: - _logger.debug( - "RESP: [%s] %s\nRESP BODY: %s\n", - resp.status_code, - resp.headers, - resp.text) - - def serialize(self, entity): - return json.dumps(entity) - - def request(self, url, method, **kwargs): - """ Send an http request with the specified characteristics. - - Wrapper around requests.request to handle tasks such as - setting headers, JSON encoding/decoding, and error handling. - """ - # Copy the kwargs so we can reuse the original in case of redirects - request_kwargs = copy.copy(kwargs) - request_kwargs.setdefault('headers', kwargs.get('headers', {})) - request_kwargs['headers']['User-Agent'] = self.USER_AGENT - if self.original_ip: - request_kwargs['headers']['Forwarded'] = "for=%s;by=%s" % ( - self.original_ip, self.USER_AGENT) - if 'body' in kwargs: - request_kwargs['headers']['Content-Type'] = 'application/json' - request_kwargs['data'] = self.serialize(kwargs['body']) - del request_kwargs['body'] - if self.cert: - request_kwargs['cert'] = self.cert - - self.http_log_req((url, method,), request_kwargs) - resp = requests.request( - method, - url, - verify=self.verify_cert, - config=self.requests_config, - **request_kwargs) - - self.http_log_resp(resp) - - if resp.status_code in (400, 401, 403, 404, 408, 409, 413, 500, 501): - _logger.debug( - "Request returned failure status: %s", - resp.status_code) - raise exceptions.from_response(resp, resp.text) - elif resp.status_code in (301, 302, 305): - # Redirected. Reissue the request to the new location. - return self.request(resp.headers['location'], method, **kwargs) - - if resp.text: - try: - body = json.loads(resp.text) - except ValueError: - body = None - _logger.debug("Could not decode JSON from body: %s" - % resp.text) - else: - _logger.debug("No body was returned.") - body = None - - return resp, body - - def _cs_request(self, url, method, **kwargs): - """ Makes an authenticated request to keystone endpoint by - concatenating self.management_url and url and passing in method and - any associated kwargs. """ - - is_management = kwargs.pop('management', True) - - if is_management and self.management_url is None: - raise exceptions.AuthorizationFailure( - 'Current authorization does not have a known management url') - - url_to_use = self.auth_url - if is_management: - url_to_use = self.management_url - - kwargs.setdefault('headers', {}) - if self.auth_token: - kwargs['headers']['X-Auth-Token'] = self.auth_token - - resp, body = self.request(url_to_use + url, method, - **kwargs) - return resp, body - - def get(self, url, **kwargs): - return self._cs_request(url, 'GET', **kwargs) - - def head(self, url, **kwargs): - return self._cs_request(url, 'HEAD', **kwargs) - - def post(self, url, **kwargs): - return self._cs_request(url, 'POST', **kwargs) - - def put(self, url, **kwargs): - return self._cs_request(url, 'PUT', **kwargs) - - def patch(self, url, **kwargs): - return self._cs_request(url, 'PATCH', **kwargs) - - def delete(self, url, **kwargs): - return self._cs_request(url, 'DELETE', **kwargs) +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import discover +from keystoneclient import session as client_session + + +def Client(version=None, unstable=False, session=None, **kwargs): + """Factory function to create a new identity service client. + + The returned client will be either a V3 or V2 client. Check the version + using the :py:attr:`~keystoneclient.v3.client.Client.version` property or + the instance's class (with instanceof). + + :param tuple version: The required version of the identity API. If + specified the client will be selected such that the + major version is equivalent and an endpoint provides + at least the specified minor version. For example to + specify the 3.1 API use ``(3, 1)``. (optional) + :param bool unstable: Accept endpoints not marked as 'stable'. (optional) + :param session: A session object to be used for communication. If one is + not provided it will be constructed from the provided + kwargs. (optional) + :type session: keystoneclient.session.Session + :param kwargs: Additional arguments are passed through to the client + that is being created. + :returns: New keystone client object. + :rtype: :py:class:`keystoneclient.v3.client.Client` or + :py:class:`keystoneclient.v2_0.client.Client` + :raises keystoneclient.exceptions.DiscoveryFailure: if the server's + response is invalid. + :raises keystoneclient.exceptions.VersionNotAvailable: if a suitable client + cannot be found. + """ + if not session: + session = client_session.Session._construct(kwargs) + + d = discover.Discover(session=session, **kwargs) + return d.create_client(version=version, unstable=unstable) diff --git a/keystoneclient/common/cms.py b/keystoneclient/common/cms.py index 93c05050c..2ee8b52ae 100644 --- a/keystoneclient/common/cms.py +++ b/keystoneclient/common/cms.py @@ -1,11 +1,53 @@ +# 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. + +"""Certificate signing functions. + +Call set_subprocess() with the subprocess module. Either Python's +subprocess or eventlet.green.subprocess can be used. + +If set_subprocess() is not called, this module will pick Python's subprocess +or eventlet.green.subprocess based on if os module is patched by eventlet. +""" + +import base64 +import errno import hashlib - import logging +import zlib + +from debtcollector import removals + +from keystoneclient import exceptions +from keystoneclient.i18n import _ subprocess = None LOG = logging.getLogger(__name__) -PKI_ANS1_PREFIX = 'MII' +PKI_ASN1_PREFIX = 'MII' +PKIZ_PREFIX = 'PKIZ_' +PKIZ_CMS_FORM = 'DER' +PKI_ASN1_FORM = 'PEM' +# Adding nosec since this fails bandit B105, 'Possible hardcoded password'. +DEFAULT_TOKEN_DIGEST_ALGORITHM = 'sha256' # nosec + + +# The openssl cms command exits with these status codes. +# See https://www.openssl.org/docs/man1.1.0/apps/cms.html#EXIT-CODES +class OpensslCmsExitStatus(object): + SUCCESS = 0 + COMMAND_OPTIONS_PARSING_ERROR = 1 + INPUT_FILE_READ_ERROR = 2 + CREATE_CMS_READ_MIME_ERROR = 3 def _ensure_subprocess(): @@ -16,53 +58,209 @@ def _ensure_subprocess(): if not subprocess: try: from eventlet import patcher - if patcher.already_patched.get('os'): + if patcher.already_patched: from eventlet.green import subprocess else: - import subprocess + import subprocess # nosec(cjschaef): we must be careful when + # using subprocess.Popen with possibly untrusted data, + # assumption is that the certificate/key files provided are + # trustworthy except ImportError: - import subprocess + import subprocess # noqa # nosec(cjschaef): we must be careful + # when using subprocess.Popen with possibly untrusted data, + # assumption is that the certificate/key files provided are + # trustworthy -def cms_verify(formatted, signing_cert_file_name, ca_file_name): +def set_subprocess(_subprocess=None): + """Set subprocess module to use. + + The subprocess could be eventlet.green.subprocess if using eventlet, + or Python's subprocess otherwise. """ - verifies the signature of the contents IAW CMS syntax + global subprocess + subprocess = _subprocess + + +def _check_files_accessible(files): + err = None + retcode = -1 + try: + for try_file in files: + with open(try_file, 'r'): + pass + except IOError as e: + # Catching IOError means there is an issue with + # the given file. + err = try_file, e.strerror + # Emulate openssl behavior, which returns with code 2 when + # access to a file failed. + retcode = OpensslCmsExitStatus.INPUT_FILE_READ_ERROR + + return retcode, err + + +def _process_communicate_handle_oserror(process, data, files): + """Wrapper around process.communicate that checks for OSError.""" + try: + output, err = process.communicate(data) + except OSError as e: + if e.errno != errno.EPIPE: + raise + # OSError with EPIPE only occurs with old Python 2.7.x versions + # http://bugs.python.org/issue10963 + + # The quick exit is typically caused by the openssl command not being + # able to read an input file, so check ourselves if can't read a file. + retcode, err = _check_files_accessible(files) + if process.stderr: + msg = process.stderr.read() + if isinstance(msg, bytes): + msg = msg.decode('utf-8') + if err: + err = (_('Hit OSError in ' + '_process_communicate_handle_oserror(): ' + '%(stderr)s\nLikely due to %(file)s: %(error)s') % + {'stderr': msg, + 'file': err[0], + 'error': err[1]}) + else: + err = (_('Hit OSError in ' + '_process_communicate_handle_oserror(): %s') % msg) + + output = '' + else: + retcode = process.poll() + if err is not None: + if isinstance(err, bytes): + err = err.decode('utf-8') + + return output, err, retcode + + +def _encoding_for_form(inform): + if inform == PKI_ASN1_FORM: + encoding = 'UTF-8' + elif inform == PKIZ_CMS_FORM: + encoding = 'hex' + else: + raise ValueError( + _('"inform" must be one of: %s') % ','.join((PKI_ASN1_FORM, + PKIZ_CMS_FORM))) + + return encoding + + +def cms_verify(formatted, signing_cert_file_name, ca_file_name, + inform=PKI_ASN1_FORM): + """Verify the signature of the contents IAW CMS syntax. + + :raises subprocess.CalledProcessError: + :raises keystoneclient.exceptions.CertificateConfigError: if certificate + is not configured + properly. """ _ensure_subprocess() - process = subprocess.Popen(["openssl", "cms", "-verify", - "-certfile", signing_cert_file_name, - "-CAfile", ca_file_name, - "-inform", "PEM", - "-nosmimecap", "-nodetach", - "-nocerts", "-noattr"], + if isinstance(formatted, str): + data = bytes(formatted, _encoding_for_form(inform)) + else: + data = formatted + process = subprocess.Popen(['openssl', 'cms', '-verify', + '-certfile', signing_cert_file_name, + '-CAfile', ca_file_name, + '-inform', 'PEM', + '-nosmimecap', '-nodetach', + '-nocerts', '-noattr'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - output, err = process.communicate(formatted) - retcode = process.poll() - if retcode: - LOG.error('Verify error: %s' % err) - raise subprocess.CalledProcessError(retcode, "openssl", output=err) + stderr=subprocess.PIPE, + close_fds=True) + output, err, retcode = _process_communicate_handle_oserror( + process, data, (signing_cert_file_name, ca_file_name)) + + # Do not log errors, as some happen in the positive thread + # instead, catch them in the calling code and log them there. + + # When invoke the openssl >= 1.1.0 with not exist file, return code should + # be 2 instead of 1 and error msg will be returned. + # You can get more from + # https://www.openssl.org/docs/man1.1.0/apps/cms.html#EXIT-CODES + # + # $ openssl cms -verify -certfile not_exist_file -CAfile + # not_exist_file -inform PEM -nosmimecap -nodetach + # -nocerts -noattr + # openssl < 1.1.0 returns + # Error opening certificate file not_exist_file + # openssl >= 1.1.0 returns + # cms: Cannot open input file not_exist_file, No such file or directory + # + if retcode == OpensslCmsExitStatus.INPUT_FILE_READ_ERROR: + if err.startswith('Error reading S/MIME message'): + raise exceptions.CMSError(err) + else: + raise exceptions.CertificateConfigError(err) + # workaround for OpenSSL >= 1.1.0, + # should return OpensslCmsExitStatus.INPUT_FILE_READ_ERROR + elif retcode == OpensslCmsExitStatus.COMMAND_OPTIONS_PARSING_ERROR: + if err.startswith('cms: Cannot open input file'): + raise exceptions.CertificateConfigError(err) + else: + raise subprocess.CalledProcessError(retcode, 'openssl', output=err) + elif retcode != OpensslCmsExitStatus.SUCCESS: + raise subprocess.CalledProcessError(retcode, 'openssl', output=err) return output -def token_to_cms(signed_text): - copy_of_text = signed_text.replace('-', '/') +def is_pkiz(token_text): + """Determine if a token is PKIZ. - formatted = "-----BEGIN CMS-----\n" - line_length = 64 - while len(copy_of_text) > 0: - if (len(copy_of_text) > line_length): - formatted += copy_of_text[:line_length] - copy_of_text = copy_of_text[line_length:] - else: - formatted += copy_of_text - copy_of_text = "" - formatted += "\n" + Checks if the string has the prefix that indicates it is a + Crypto Message Syntax, Z compressed token. + """ + return token_text.startswith(PKIZ_PREFIX) - formatted += "-----END CMS-----\n" - return formatted +def pkiz_sign(text, + signing_cert_file_name, + signing_key_file_name, + compression_level=6, + message_digest=DEFAULT_TOKEN_DIGEST_ALGORITHM): + signed = cms_sign_data(text, + signing_cert_file_name, + signing_key_file_name, + PKIZ_CMS_FORM, + message_digest=message_digest) + + compressed = zlib.compress(signed, compression_level) + encoded = PKIZ_PREFIX + base64.urlsafe_b64encode( + compressed).decode('utf-8') + return encoded + + +def pkiz_uncompress(signed_text): + text = signed_text[len(PKIZ_PREFIX):].encode('utf-8') + unencoded = base64.urlsafe_b64decode(text) + uncompressed = zlib.decompress(unencoded) + return uncompressed + + +def pkiz_verify(signed_text, signing_cert_file_name, ca_file_name): + uncompressed = pkiz_uncompress(signed_text) + return cms_verify(uncompressed, signing_cert_file_name, ca_file_name, + inform=PKIZ_CMS_FORM) + + +def token_to_cms(signed_text): + """Convert a custom formatted token to a PEM-formatted token. + + See documentation for cms_to_token() for details on the custom formatting. + """ + copy_of_text = signed_text.replace('-', '/') + + lines = ['-----BEGIN CMS-----'] + lines += [copy_of_text[n:n + 64] for n in range(0, len(copy_of_text), 64)] + lines.append('-----END CMS-----\n') + return '\n'.join(lines) def verify_token(token, signing_cert_file_name, ca_file_name): @@ -71,80 +269,154 @@ def verify_token(token, signing_cert_file_name, ca_file_name): ca_file_name) -def is_ans1_token(token): - ''' +def is_asn1_token(token): + """Determine if a token appears to be PKI-based. + thx to ayoung for sorting this out. - base64 decoded hex representation of MII is 3082 - In [3]: binascii.hexlify(base64.b64decode('MII=')) - Out[3]: '3082' + base64 decoded hex representation of MII is 3082:: + + In [3]: binascii.hexlify(base64.b64decode('MII=')) + Out[3]: '3082' re: http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf - pg4: For tags from 0 to 30 the first octet is the identfier - pg10: Hex 30 means sequence, followed by the length of that sequence. - pg5: Second octet is the length octet - first bit indicates short or long form, next 7 bits encode the number - of subsequent octets that make up the content length octets as an - unsigned binary int + :: + + pg4: For tags from 0 to 30 the first octet is the identfier + pg10: Hex 30 means sequence, followed by the length of that sequence. + pg5: Second octet is the length octet + first bit indicates short or long form, next 7 bits encode the + number of subsequent octets that make up the content length octets + as an unsigned binary int - 82 = 10000010 (first bit indicates long form) - 0000010 = 2 octets of content length - so read the next 2 octets to get the length of the content. + 82 = 10000010 (first bit indicates long form) + 0000010 = 2 octets of content length + so read the next 2 octets to get the length of the content. In the case of a very large content length there could be a requirement to have more than 2 octets to designate the content length, therefore requiring us to check for MIM, MIQ, etc. - In [4]: base64.b64encode(binascii.a2b_hex('3083')) - Out[4]: 'MIM=' - In [5]: base64.b64encode(binascii.a2b_hex('3084')) - Out[5]: 'MIQ=' - Checking for MI would become invalid at 16 octets of content length - 10010000 = 90 - In [6]: base64.b64encode(binascii.a2b_hex('3090')) - Out[6]: 'MJA=' - Checking for just M is insufficient + + :: + + In [4]: base64.b64encode(binascii.a2b_hex('3083')) + Out[4]: 'MIM=' + In [5]: base64.b64encode(binascii.a2b_hex('3084')) + Out[5]: 'MIQ=' + Checking for MI would become invalid at 16 octets of content length + 10010000 = 90 + In [6]: base64.b64encode(binascii.a2b_hex('3090')) + Out[6]: 'MJA=' + Checking for just M is insufficient But we will only check for MII: - Max length of the content using 2 octets is 7FFF or 32767 + Max length of the content using 2 octets is 3FFF or 16383. + It's not practical to support a token of this length or greater in http therefore, we will check for MII only and ignore the case of larger tokens - ''' - return token[:3] == PKI_ANS1_PREFIX + """ + return token[:3] == PKI_ASN1_PREFIX + + +@removals.remove(message='Use is_asn1_token() instead.', version='1.7.0', + removal_version='2.0.0') +def is_ans1_token(token): + """Deprecated. + + This function is deprecated as of the 1.7.0 release in favor of + :func:`is_asn1_token` and may be removed in the 2.0.0 release. + """ + return is_asn1_token(token) + + +def cms_sign_text(data_to_sign, signing_cert_file_name, signing_key_file_name, + message_digest=DEFAULT_TOKEN_DIGEST_ALGORITHM): + return cms_sign_data(data_to_sign, signing_cert_file_name, + signing_key_file_name, message_digest=message_digest) + +def cms_sign_data(data_to_sign, signing_cert_file_name, signing_key_file_name, + outform=PKI_ASN1_FORM, + message_digest=DEFAULT_TOKEN_DIGEST_ALGORITHM): + """Use OpenSSL to sign a document. -def cms_sign_text(text, signing_cert_file_name, signing_key_file_name): - """ Uses OpenSSL to sign a document Produces a Base64 encoding of a DER formatted CMS Document http://en.wikipedia.org/wiki/Cryptographic_Message_Syntax + + :param data_to_sign: data to sign + :param signing_cert_file_name: path to the X509 certificate containing + the public key associated with the private key used to sign the data + :param signing_key_file_name: path to the private key used to sign + the data + :param outform: Format for the signed document PKIZ_CMS_FORM or + PKI_ASN1_FORM + :param message_digest: Digest algorithm to use when signing or resigning + """ _ensure_subprocess() - process = subprocess.Popen(["openssl", "cms", "-sign", - "-signer", signing_cert_file_name, - "-inkey", signing_key_file_name, - "-outform", "PEM", - "-nosmimecap", "-nodetach", - "-nocerts", "-noattr"], + if isinstance(data_to_sign, str): + data = bytes(data_to_sign, encoding='utf-8') + else: + data = data_to_sign + process = subprocess.Popen(['openssl', 'cms', '-sign', + '-signer', signing_cert_file_name, + '-inkey', signing_key_file_name, + '-outform', 'PEM', + '-nosmimecap', '-nodetach', + '-nocerts', '-noattr', + '-md', message_digest, ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - output, err = process.communicate(text) - retcode = process.poll() - if retcode or "Error" in err: - LOG.error('Signing error: %s' % err) - raise subprocess.CalledProcessError(retcode, "openssl") - return output + stderr=subprocess.PIPE, + close_fds=True) + output, err, retcode = _process_communicate_handle_oserror( + process, data, (signing_cert_file_name, signing_key_file_name)) -def cms_sign_token(text, signing_cert_file_name, signing_key_file_name): - output = cms_sign_text(text, signing_cert_file_name, signing_key_file_name) + if retcode != OpensslCmsExitStatus.SUCCESS or ('Error' in err): + if retcode == OpensslCmsExitStatus.CREATE_CMS_READ_MIME_ERROR: + LOG.error('Signing error: Unable to load certificate - ' + 'ensure you have configured PKI with ' + '"keystone-manage pki_setup"') + else: + LOG.error('Signing error: %s', err) + raise subprocess.CalledProcessError(retcode, 'openssl') + if outform == PKI_ASN1_FORM: + return output.decode('utf-8') + else: + return output + + +def cms_sign_token(text, signing_cert_file_name, signing_key_file_name, + message_digest=DEFAULT_TOKEN_DIGEST_ALGORITHM): + output = cms_sign_data(text, signing_cert_file_name, signing_key_file_name, + message_digest=message_digest) return cms_to_token(output) def cms_to_token(cms_text): - - start_delim = "-----BEGIN CMS-----" - end_delim = "-----END CMS-----" + """Convert a CMS-signed token in PEM format to a custom URL-safe format. + + The conversion consists of replacing '/' char in the PEM-formatted token + with the '-' char and doing other such textual replacements to make the + result marshallable via HTTP. The return value can thus be used as the + value of a HTTP header such as "X-Auth-Token". + + This ad-hoc conversion is an unfortunate oversight since the returned + value now does not conform to any of the standard variants of base64 + encoding. It would have been better to use base64url encoding (either on + the PEM formatted text or, perhaps even better, on the inner CMS-signed + binary value without any PEM formatting). In any case, the same conversion + is done in reverse in the other direction (for token verification), so + there are no correctness issues here. Note that the non-standard encoding + of the token will be preserved so as to not break backward compatibility. + + The conversion issue is detailed by the code author in a blog post at + http://adam.younglogic.com/2014/02/compressed-tokens/. + """ + start_delim = '-----BEGIN CMS-----' + end_delim = '-----END CMS-----' signed_text = cms_text signed_text = signed_text.replace('/', '-') signed_text = signed_text.replace(start_delim, '') @@ -154,15 +426,18 @@ def cms_to_token(cms_text): return signed_text -def cms_hash_token(token_id): - """ - return: for ans1_token, returns the hash of the passed in token +def cms_hash_token(token_id, mode='md5'): + """Hash PKI tokens. + + return: for asn1 or pkiz tokens, returns the hash of the passed in token otherwise, returns what it was passed in. """ if token_id is None: return None - if is_ans1_token(token_id): - hasher = hashlib.md5() + if is_asn1_token(token_id) or is_pkiz(token_id): + hasher = hashlib.new(mode) + if isinstance(token_id, str): + token_id = token_id.encode('utf-8') hasher.update(token_id) return hasher.hexdigest() else: diff --git a/keystoneclient/contrib/bootstrap/__init__.py b/keystoneclient/contrib/auth/__init__.py similarity index 100% rename from keystoneclient/contrib/bootstrap/__init__.py rename to keystoneclient/contrib/auth/__init__.py diff --git a/keystoneclient/middleware/__init__.py b/keystoneclient/contrib/auth/v3/__init__.py similarity index 100% rename from keystoneclient/middleware/__init__.py rename to keystoneclient/contrib/auth/v3/__init__.py diff --git a/keystoneclient/contrib/auth/v3/oidc.py b/keystoneclient/contrib/auth/v3/oidc.py new file mode 100644 index 000000000..3884293cc --- /dev/null +++ b/keystoneclient/contrib/auth/v3/oidc.py @@ -0,0 +1,209 @@ +# 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 oslo_config import cfg + +from keystoneclient import access +from keystoneclient.auth.identity.v3 import federated + + +class OidcPassword(federated.FederatedBaseAuth): + """Implement authentication plugin for OpenID Connect protocol. + + OIDC or OpenID Connect is a protocol for federated authentication. + + The OpenID Connect specification can be found at:: + ``http://openid.net/specs/openid-connect-core-1_0.html`` + """ + + @classmethod + def get_options(cls): + options = super(OidcPassword, cls).get_options() + options.extend([ + cfg.StrOpt('username', help='Username'), + cfg.StrOpt('password', secret=True, help='Password'), + cfg.StrOpt('client-id', help='OAuth 2.0 Client ID'), + cfg.StrOpt('client-secret', secret=True, + help='OAuth 2.0 Client Secret'), + cfg.StrOpt('access-token-endpoint', + help='OpenID Connect Provider Token Endpoint'), + cfg.StrOpt('scope', default="profile", + help='OpenID Connect scope that is requested from OP') + ]) + return options + + def __init__(self, auth_url, identity_provider, protocol, + username, password, client_id, client_secret, + access_token_endpoint, scope='profile', + grant_type='password'): + """The OpenID Connect plugin. + + 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 username: Username used to authenticate + :type username: string + + :param password: Password used to authenticate + :type password: string + + :param client_id: OAuth 2.0 Client ID + :type client_id: string + + :param client_secret: OAuth 2.0 Client Secret + :type client_secret: string + + :param access_token_endpoint: OpenID Connect Provider Token Endpoint, + for example: + https://localhost:8020/oidc/OP/token + :type access_token_endpoint: string + + :param scope: OpenID Connect scope that is requested from OP, + defaults to "profile", for example: "profile email" + :type scope: string + + :param grant_type: OpenID Connect grant type, it represents the flow + that is used to talk to the OP. Valid values are: + "authorization_code", "refresh_token", or + "password". + :type grant_type: string + """ + super(OidcPassword, self).__init__(auth_url, identity_provider, + protocol) + self._username = username + self._password = password + self.client_id = client_id + self.client_secret = client_secret + self.access_token_endpoint = access_token_endpoint + self.scope = scope + self.grant_type = grant_type + + @property + def username(self): + # Override to remove deprecation. + return self._username + + @username.setter + def username(self, value): + # Override to remove deprecation. + self._username = value + + @property + def password(self): + # Override to remove deprecation. + return self._password + + @password.setter + def password(self, value): + # Override to remove deprecation. + self._password = value + + def get_unscoped_auth_ref(self, session): + """Authenticate with OpenID Connect and get back claims. + + This is a multi-step process. First an access token must be retrieved, + to do this, the username and password, the OpenID Connect client ID + and secret, and the access token endpoint must be known. + + Secondly, we then exchange the access token upon accessing the + protected Keystone endpoint (federated auth URL). This will trigger + the OpenID Connect Provider to perform a user introspection and + retrieve information (specified in the scope) about the user in + the form of an OpenID Connect Claim. These claims will be sent + to Keystone in the form of environment variables. + + :param session: a session object to send out HTTP requests. + :type session: keystoneclient.session.Session + + :returns: a token data representation + :rtype: :py:class:`keystoneclient.access.AccessInfo` + """ + # get an access token + client_auth = (self.client_id, self.client_secret) + payload = {'grant_type': self.grant_type, 'username': self.username, + 'password': self.password, 'scope': self.scope} + response = self._get_access_token(session, client_auth, payload, + self.access_token_endpoint) + access_token = response.json()['access_token'] + + # use access token against protected URL + headers = {'Authorization': 'Bearer ' + access_token} + response = self._get_keystone_token(session, headers, + self.federated_token_url) + + # grab the unscoped token + token = response.headers['X-Subject-Token'] + token_json = response.json()['token'] + return access.AccessInfoV3(token, **token_json) + + def _get_access_token(self, session, client_auth, payload, + access_token_endpoint): + """Exchange a variety of user supplied values for an access token. + + :param session: a session object to send out HTTP requests. + :type session: keystoneclient.session.Session + + :param client_auth: a tuple representing client id and secret + :type client_auth: tuple + + :param payload: a dict containing various OpenID Connect values, for + example:: + {'grant_type': 'password', 'username': self.username, + 'password': self.password, 'scope': self.scope} + :type payload: dict + + :param access_token_endpoint: URL to use to get an access token, for + example: https://localhost/oidc/token + :type access_token_endpoint: string + """ + op_response = session.post(self.access_token_endpoint, + requests_auth=client_auth, + data=payload, + authenticated=False) + return op_response + + def _get_keystone_token(self, session, headers, federated_token_url): + r"""Exchange an acess token for a keystone token. + + By Sending the access token in an `Authorization: Bearer` header, to + an OpenID Connect protected endpoint (Federated Token URL). The + OpenID Connect server will use the access token to look up information + about the authenticated user (this technique is called instrospection). + The output of the instrospection will be an OpenID Connect Claim, that + will be used against the mapping engine. Should the mapping engine + succeed, a Keystone token will be presented to the user. + + :param session: a session object to send out HTTP requests. + :type session: keystoneclient.session.Session + + :param headers: an Authorization header containing the access token. + :type headers_: dict + + :param federated_auth_url: Protected URL for federated authentication, + for example: https://localhost:5000/v3/\ + OS-FEDERATION/identity_providers/bluepages/\ + protocols/oidc/auth + :type federated_auth_url: string + """ + auth_response = session.post(self.federated_token_url, + headers=headers, + authenticated=False) + return auth_response diff --git a/keystoneclient/contrib/auth/v3/saml2.py b/keystoneclient/contrib/auth/v3/saml2.py new file mode 100644 index 000000000..acf3f0513 --- /dev/null +++ b/keystoneclient/contrib/auth/v3/saml2.py @@ -0,0 +1,920 @@ +# 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 urllib.parse +import uuid + +from lxml import etree # nosec(cjschaef): used to create xml, not parse it +from oslo_config import cfg + +from keystoneclient import access +from keystoneclient.auth.identity import v3 +from keystoneclient import exceptions +from keystoneclient.i18n import _ + + +class _BaseSAMLPlugin(v3.AuthConstructor): + + HTTP_MOVED_TEMPORARILY = 302 + HTTP_SEE_OTHER = 303 + + PROTOCOL = 'saml2' + + @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) + + @property + def token_url(self): + """Return 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 + + @classmethod + def get_options(cls): + options = super(_BaseSAMLPlugin, cls).get_options() + options.extend([ + cfg.StrOpt('identity-provider', help="Identity Provider's name"), + cfg.StrOpt('identity-provider-url', + help="Identity Provider's URL"), + cfg.StrOpt('username', dest='username', help='Username', + deprecated_name='user-name'), + cfg.StrOpt('password', secret=True, help='Password') + ]) + return options + + +class Saml2UnscopedTokenAuthMethod(v3.AuthMethod): + _method_parameters = [] + + def get_auth_data(self, session, auth, headers, **kwargs): + raise exceptions.MethodNotImplemented(_('This method should never ' + 'be called')) + + +class Saml2UnscopedToken(_BaseSAMLPlugin): + 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 + `_ on ECP. + + Reference the `SAML2 ECP specification `_. + + Currently only HTTPBasicAuth mechanism is available for the IdP + authenication. + + :param auth_url: URL of the Identity Service + :type auth_url: string + + :param identity_provider: name of the Identity Provider the client will + authenticate against. This parameter will be used + to build a dynamic URL used to obtain unscoped + OpenStack token. + :type identity_provider: string + + :param identity_provider_url: An Identity Provider URL, where the SAML2 + authn request will be sent. + :type identity_provider_url: string + + :param username: User's login + :type username: string + + :param password: User's password + :type password: string + + """ + + _auth_method_class = Saml2UnscopedTokenAuthMethod + + SAML2_HEADER_INDEX = 0 + ECP_SP_EMPTY_REQUEST_HEADERS = { + 'Accept': 'text/html, application/vnd.paos+xml', + 'PAOS': ('ver="urn:liberty:paos:2003-08";"urn:oasis:names:tc:' + 'SAML:2.0:profiles:SSO:ecp"') + } + + ECP_SP_SAML2_REQUEST_HEADERS = { + 'Content-Type': 'application/vnd.paos+xml' + } + + ECP_SAML2_NAMESPACES = { + 'ecp': 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp', + 'S': 'http://schemas.xmlsoap.org/soap/envelope/', + 'paos': 'urn:liberty:paos:2003-08' + } + + ECP_RELAY_STATE = '//ecp:RelayState' + + ECP_SERVICE_PROVIDER_CONSUMER_URL = ('/S:Envelope/S:Header/paos:Request/' + '@responseConsumerURL') + + ECP_IDP_CONSUMER_URL = ('/S:Envelope/S:Header/ecp:Response/' + '@AssertionConsumerServiceURL') + + SOAP_FAULT = """ + + + + S:Server + responseConsumerURL from SP and + assertionConsumerServiceURL from IdP do not match + + + + + """ + + def __init__(self, auth_url, + identity_provider, + identity_provider_url, + username, password, + **kwargs): + super(Saml2UnscopedToken, self).__init__(auth_url=auth_url, **kwargs) + self.identity_provider = identity_provider + self.identity_provider_url = identity_provider_url + self._username, self._password = username, password + + @property + def username(self): + # Override to remove deprecation. + return self._username + + @username.setter + def username(self, value): + # Override to remove deprecation. + self._username = value + + @property + def password(self): + # Override to remove deprecation. + return self._password + + @password.setter + def password(self, value): + # Override to remove deprecation. + self._password = value + + def _handle_http_ecp_redirect(self, session, response, method, **kwargs): + if response.status_code not in (self.HTTP_MOVED_TEMPORARILY, + self.HTTP_SEE_OTHER): + return response + + location = response.headers['location'] + return session.request(location, method, authenticated=False, + **kwargs) + + def _prepare_idp_saml2_request(self, saml2_authn_request): + header = saml2_authn_request[self.SAML2_HEADER_INDEX] + saml2_authn_request.remove(header) + + def _check_consumer_urls(self, session, sp_response_consumer_url, + idp_sp_response_consumer_url): + """Check if consumer URLs issued by SP and IdP are equal. + + In the initial SAML2 authn Request issued by a Service Provider + there is a url called ``consumer url``. A trusted Identity Provider + should issue identical url. If the URLs are not equal the federated + authn process should be interrupted and the user should be warned. + + :param session: session object to send out HTTP requests. + :type session: keystoneclient.session.Session + :param sp_response_consumer_url: consumer URL issued by a SP + :type sp_response_consumer_url: string + :param idp_sp_response_consumer_url: consumer URL issued by an IdP + :type idp_sp_response_consumer_url: string + + """ + if sp_response_consumer_url != idp_sp_response_consumer_url: + # send fault message to the SP, discard the response + session.post(sp_response_consumer_url, data=self.SOAP_FAULT, + headers=self.ECP_SP_SAML2_REQUEST_HEADERS, + authenticated=False) + + # prepare error message and raise an exception. + msg = _("Consumer URLs from Service Provider %(service_provider)s " + "%(sp_consumer_url)s and Identity Provider " + "%(identity_provider)s %(idp_consumer_url)s are not equal") + msg = msg % { + 'service_provider': self.token_url, + 'sp_consumer_url': sp_response_consumer_url, + 'identity_provider': self.identity_provider, + 'idp_consumer_url': idp_sp_response_consumer_url + } + + raise exceptions.ValidationError(msg) + + def _send_service_provider_request(self, session): + """Initial HTTP GET request to the SAML2 protected endpoint. + + It's crucial to include HTTP headers indicating that the client is + willing to take advantage of the ECP SAML2 extension and receive data + as the SOAP. + Unlike standard authentication methods in the OpenStack Identity, + the client accesses:: + ``/v3/OS-FEDERATION/identity_providers/{identity_providers}/ + protocols/{protocol}/auth`` + + After a successful HTTP call the HTTP response should include SAML2 + authn request in the XML format. + + If a HTTP response contains ``X-Subject-Token`` in the headers and + the response body is a valid JSON assume the user was already + authenticated and Keystone returned a valid unscoped token. + Return True indicating the user was already authenticated. + + :param session: a session object to send out HTTP requests. + :type session: keystoneclient.session.Session + + """ + sp_response = session.get(self.token_url, + headers=self.ECP_SP_EMPTY_REQUEST_HEADERS, + authenticated=False) + + if 'X-Subject-Token' in sp_response.headers: + self.authenticated_response = sp_response + return True + + try: + self.saml2_authn_request = etree.XML(sp_response.content) + except etree.XMLSyntaxError as e: + msg = _("SAML2: Error parsing XML returned " + "from Service Provider, reason: %s") % e + raise exceptions.AuthorizationFailure(msg) + + relay_state = self.saml2_authn_request.xpath( + self.ECP_RELAY_STATE, namespaces=self.ECP_SAML2_NAMESPACES) + self.relay_state = self._first(relay_state) + + sp_response_consumer_url = self.saml2_authn_request.xpath( + self.ECP_SERVICE_PROVIDER_CONSUMER_URL, + namespaces=self.ECP_SAML2_NAMESPACES) + self.sp_response_consumer_url = self._first(sp_response_consumer_url) + return False + + def _send_idp_saml2_authn_request(self, session): + """Present modified SAML2 authn assertion from the Service Provider.""" + self._prepare_idp_saml2_request(self.saml2_authn_request) + idp_saml2_authn_request = self.saml2_authn_request + + # Currently HTTPBasicAuth method is hardcoded into the plugin + idp_response = session.post( + self.identity_provider_url, + headers={'Content-type': 'text/xml'}, + data=etree.tostring(idp_saml2_authn_request), + requests_auth=(self.username, self.password), + authenticated=False, log=False) + + try: + self.saml2_idp_authn_response = etree.XML(idp_response.content) + except etree.XMLSyntaxError as e: + msg = _("SAML2: Error parsing XML returned " + "from Identity Provider, reason: %s") % e + raise exceptions.AuthorizationFailure(msg) + + idp_response_consumer_url = self.saml2_idp_authn_response.xpath( + self.ECP_IDP_CONSUMER_URL, + namespaces=self.ECP_SAML2_NAMESPACES) + + self.idp_response_consumer_url = self._first(idp_response_consumer_url) + + self._check_consumer_urls(session, self.idp_response_consumer_url, + self.sp_response_consumer_url) + + def _send_service_provider_saml2_authn_response(self, session): + """Present SAML2 assertion to the Service Provider. + + The assertion is issued by a trusted Identity Provider for the + authenticated user. This function directs the HTTP request to SP + managed URL, for instance: ``https://:/Shibboleth.sso/ + SAML2/ECP``. + Upon success there's a session created and access to the protected + resource is granted. Many implementations of the SP return HTTP 302/303 + status code pointing to the protected URL (``https://:/v3/ + OS-FEDERATION/identity_providers/{identity_provider}/protocols/ + {protocol_id}/auth`` in this case). Saml2 plugin should point to that + URL again, with HTTP GET method, expecting an unscoped token. + + :param session: a session object to send out HTTP requests. + + """ + self.saml2_idp_authn_response[0][0] = self.relay_state + + response = session.post( + self.idp_response_consumer_url, + headers=self.ECP_SP_SAML2_REQUEST_HEADERS, + data=etree.tostring(self.saml2_idp_authn_response), + authenticated=False, redirect=False) + + # Don't follow HTTP specs - after the HTTP 302/303 response don't + # repeat the call directed to the Location URL. In this case, this is + # an indication that saml2 session is now active and protected resource + # can be accessed. + response = self._handle_http_ecp_redirect( + session, response, method='GET', + headers=self.ECP_SP_SAML2_REQUEST_HEADERS) + + self.authenticated_response = response + + def _get_unscoped_token(self, session): + """Get unscoped OpenStack token after federated authentication. + + This is a multi-step process including multiple HTTP requests. + + The federated authentication consists of:: + * HTTP GET request to the Identity Service (acting as a Service + Provider). Client utilizes URL:: + ``/v3/OS-FEDERATION/identity_providers/{identity_provider}/ + protocols/saml2/auth``. + It's crucial to include HTTP headers indicating we are expecting + SOAP message in return. + Service Provider should respond with such SOAP message. + This step is handed by a method + ``Saml2UnscopedToken_send_service_provider_request()`` + + * HTTP POST request to the external Identity Provider service with + ECP extension enabled. The content sent is a header removed SOAP + message returned from the Service Provider. It's also worth noting + that ECP extension to the SAML2 doesn't define authentication method. + The most popular is HttpBasicAuth with just user and password. + Other possibilities could be X509 certificates or Kerberos. + Upon successful authentication the user should receive a SAML2 + assertion. + This step is handed by a method + ``Saml2UnscopedToken_send_idp_saml2_authn_request(session)`` + + * HTTP POST request again to the Service Provider. The body of the + request includes SAML2 assertion issued by a trusted Identity + Provider. The request should be sent to the Service Provider + consumer url specified in the SAML2 assertion. + Providing the authentication was successful and both Service Provider + and Identity Providers are trusted to each other, the Service + Provider will issue an unscoped token with a list of groups the + federated user is a member of. + This step is handed by a method + ``Saml2UnscopedToken_send_service_provider_saml2_authn_response()`` + + Unscoped token example:: + + { + "token": { + "methods": [ + "saml2" + ], + "user": { + "id": "username%40example.com", + "name": "username@example.com", + "OS-FEDERATION": { + "identity_provider": "ACME", + "protocol": "saml2", + "groups": [ + {"id": "abc123"}, + {"id": "bcd234"} + ] + } + } + } + } + + + :param session : a session object to send out HTTP requests. + :type session: keystoneclient.session.Session + + :returns: (token, token_json) + + """ + saml_authenticated = self._send_service_provider_request(session) + if not saml_authenticated: + self._send_idp_saml2_authn_request(session) + self._send_service_provider_saml2_authn_response(session) + return (self.authenticated_response.headers['X-Subject-Token'], + self.authenticated_response.json()['token']) + + def get_auth_ref(self, session, **kwargs): + """Authenticate via SAML2 protocol and retrieve unscoped token. + + This is a multi-step process where a client does federated authn + receives an unscoped token. + + Federated authentication utilizing SAML2 Enhanced Client or Proxy + extension. See ``Saml2UnscopedToken_get_unscoped_token()`` + for more information on that step. + Upon successful authentication and assertion mapping an + unscoped token is returned and stored within the plugin object for + further use. + + :param session : a session object to send out HTTP requests. + :type session: keystoneclient.session.Session + + :return: an object with scoped token's id and unscoped token json + included. + :rtype: :py:class:`keystoneclient.access.AccessInfoV3` + + """ + token, token_json = self._get_unscoped_token(session) + return access.AccessInfoV3(token, + **token_json) + + +class ADFSUnscopedToken(_BaseSAMLPlugin): + """Authentication plugin for Microsoft ADFS2.0 IdPs. + + :param auth_url: URL of the Identity Service + :type auth_url: string + + :param identity_provider: name of the Identity Provider the client will + authenticate against. This parameter will be used + to build a dynamic URL used to obtain unscoped + OpenStack token. + :type identity_provider: string + + :param identity_provider_url: An Identity Provider URL, where the SAML2 + authentication request will be sent. + :type identity_provider_url: string + + :param service_provider_endpoint: Endpoint where an assertion is being + sent, for instance: ``https://host.domain/Shibboleth.sso/ADFS`` + :type service_provider_endpoint: string + + :param username: User's login + :type username: string + + :param password: User's password + :type password: string + + """ + + _auth_method_class = Saml2UnscopedTokenAuthMethod + + 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, **kwargs): + super(ADFSUnscopedToken, self).__init__(auth_url=auth_url, **kwargs) + self.identity_provider = identity_provider + self.identity_provider_url = identity_provider_url + self.service_provider_endpoint = service_provider_endpoint + self._username, self._password = username, password + + @property + def username(self): + # Override to remove deprecation. + return self._username + + @username.setter + def username(self, value): + # Override to remove deprecation. + self._username = value + + @property + def password(self): + # Override to remove deprecation. + return self._password + + @password.setter + def password(self, value): + # Override to remove deprecation. + self._password = value + + @classmethod + def get_options(cls): + options = super(ADFSUnscopedToken, cls).get_options() + + options.extend([ + cfg.StrOpt('service-provider-endpoint', + help="Service Provider's Endpoint") + ]) + return options + + def _cookies(self, session): + """Check if cookie jar is not empty. + + keystoneclient.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: # nosec(cjschaef): fetch cookies from + # underylying requests.Session object, or fail trying + 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 ``keystoneclient`` + doesn't have mechanisms for reusing such tokens (every time ADFS authn + method is called, keystoneclient will login with the ADFS instance). + + :param fmt: Datetime format for specifying string format of a date. + It should not be changed if the method is going to be used + for building the ADFS security token request. + :type fmt: string + + """ + date_created = datetime.datetime.utcnow() + date_expires = date_created + datetime.timedelta( + seconds=self.DEFAULT_ADFS_TOKEN_EXPIRATION) + return [_time.strftime(fmt) for _time in (date_created, date_expires)] + + def _prepare_adfs_request(self): + """Build the ADFS Request Security Token SOAP message. + + Some values like username or password are inserted in the request. + + """ + WSS_SECURITY_NAMESPACE = { + 'o': ('http://docs.oasis-open.org/wss/2004/01/oasis-200401-' + 'wss-wssecurity-secext-1.0.xsd') + } + + TRUST_NAMESPACE = { + 'trust': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512' + } + + WSP_NAMESPACE = { + 'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy' + } + + WSA_NAMESPACE = { + 'wsa': 'http://www.w3.org/2005/08/addressing' + } + + root = etree.Element( + '{http://www.w3.org/2003/05/soap-envelope}Envelope', + nsmap=self.NAMESPACES) + + header = etree.SubElement( + root, '{http://www.w3.org/2003/05/soap-envelope}Header') + action = etree.SubElement( + header, "{http://www.w3.org/2005/08/addressing}Action") + action.set( + "{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1") + action.text = ('http://docs.oasis-open.org/ws-sx/ws-trust/200512' + '/RST/Issue') + + messageID = etree.SubElement( + header, '{http://www.w3.org/2005/08/addressing}MessageID') + messageID.text = 'urn:uuid:' + uuid.uuid4().hex + replyID = etree.SubElement( + header, '{http://www.w3.org/2005/08/addressing}ReplyTo') + address = etree.SubElement( + replyID, '{http://www.w3.org/2005/08/addressing}Address') + address.text = 'http://www.w3.org/2005/08/addressing/anonymous' + + to = etree.SubElement( + header, '{http://www.w3.org/2005/08/addressing}To') + to.set("{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1") + + security = etree.SubElement( + header, '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' + 'wss-wssecurity-secext-1.0.xsd}Security', + nsmap=WSS_SECURITY_NAMESPACE) + + security.set( + "{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1") + + timestamp = etree.SubElement( + security, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' + 'wss-wssecurity-utility-1.0.xsd}Timestamp')) + timestamp.set( + ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' + 'wss-wssecurity-utility-1.0.xsd}Id'), '_0') + + created = etree.SubElement( + timestamp, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' + 'wss-wssecurity-utility-1.0.xsd}Created')) + + expires = etree.SubElement( + timestamp, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' + 'wss-wssecurity-utility-1.0.xsd}Expires')) + + created.text, expires.text = self._token_dates() + + usernametoken = etree.SubElement( + security, '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' + 'wss-wssecurity-secext-1.0.xsd}UsernameToken') + usernametoken.set( + ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-' + 'wssecurity-utility-1.0.xsd}u'), "uuid-%s-1" % uuid.uuid4().hex) + + username = etree.SubElement( + usernametoken, ('{http://docs.oasis-open.org/wss/2004/01/oasis-' + '200401-wss-wssecurity-secext-1.0.xsd}Username')) + password = etree.SubElement( + usernametoken, ('{http://docs.oasis-open.org/wss/2004/01/oasis-' + '200401-wss-wssecurity-secext-1.0.xsd}Password'), + Type=('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-' + 'username-token-profile-1.0#PasswordText')) + + body = etree.SubElement( + root, "{http://www.w3.org/2003/05/soap-envelope}Body") + + request_security_token = etree.SubElement( + body, ('{http://docs.oasis-open.org/ws-sx/ws-trust/200512}' + 'RequestSecurityToken'), nsmap=TRUST_NAMESPACE) + + applies_to = etree.SubElement( + request_security_token, + '{http://schemas.xmlsoap.org/ws/2004/09/policy}AppliesTo', + nsmap=WSP_NAMESPACE) + + endpoint_reference = etree.SubElement( + applies_to, + '{http://www.w3.org/2005/08/addressing}EndpointReference', + nsmap=WSA_NAMESPACE) + + wsa_address = etree.SubElement( + endpoint_reference, + '{http://www.w3.org/2005/08/addressing}Address') + + keytype = etree.SubElement( + request_security_token, + '{http://docs.oasis-open.org/ws-sx/ws-trust/200512}KeyType') + keytype.text = ('http://docs.oasis-open.org/ws-sx/' + 'ws-trust/200512/Bearer') + + request_type = etree.SubElement( + request_security_token, + '{http://docs.oasis-open.org/ws-sx/ws-trust/200512}RequestType') + request_type.text = ('http://docs.oasis-open.org/ws-sx/' + 'ws-trust/200512/Issue') + token_type = etree.SubElement( + request_security_token, + '{http://docs.oasis-open.org/ws-sx/ws-trust/200512}TokenType') + token_type.text = 'urn:oasis:names:tc:SAML:1.0:assertion' + + # After constructing the request, let's plug in some values + username.text = self.username + password.text = self.password + to.text = self.identity_provider_url + wsa_address.text = self.service_provider_endpoint + + self.prepared_request = root + + def _get_adfs_security_token(self, session): + """Send ADFS Security token to the ADFS server. + + Store the result in the instance attribute and raise an exception in + case the response is not valid XML data. + + If a user cannot authenticate due to providing bad credentials, the + ADFS2.0 server will return a HTTP 500 response and a XML Fault message. + If ``exceptions.InternalServerError`` is caught, the method tries to + parse the XML response. + If parsing is unsuccessful, an ``exceptions.AuthorizationFailure`` is + raised with a reason from the XML fault. Otherwise an original + ``exceptions.InternalServerError`` is re-raised. + + :param session : a session object to send out HTTP requests. + :type session: keystoneclient.session.Session + + :raises keystoneclient.exceptions.AuthorizationFailure: when HTTP + response from the ADFS server is not a valid XML ADFS security + token. + :raises keystoneclient.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: keystoneclient.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: keystoneclient.session.Session + + :raises keystoneclient.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.token_url, + authenticated=False) + + def _get_unscoped_token(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) . + Step handled by ``ADFSUnscopedToken._prepare_adfs_request()`` method. + + * Send ADFS Security token to the ADFS server. Step handled by + ``ADFSUnscopedToken._get_adfs_security_token()`` method. + + * 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 ``ADFSUnscopedToken._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 + ``ADFSUnscopedToken._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 + ``ADFSUnscopedToken._access_service_provider()`` method. + + :param session : a session object to send out HTTP requests. + :type session: keystoneclient.session.Session + + :returns: (Unscoped federated token, token JSON body) + + """ + 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) + + try: + return (self.authenticated_response.headers['X-Subject-Token'], + self.authenticated_response.json()['token']) + except (KeyError, ValueError): + raise exceptions.InvalidResponse( + response=self.authenticated_response) + + def get_auth_ref(self, session, **kwargs): + token, token_json = self._get_unscoped_token(session) + return access.AccessInfoV3(token, **token_json) + + +class Saml2ScopedTokenMethod(v3.TokenMethod): + _method_name = 'saml2' + + def get_auth_data(self, session, auth, headers, **kwargs): + """Build and return request body for token scoping step.""" + t = super(Saml2ScopedTokenMethod, self).get_auth_data( + session, auth, headers, **kwargs) + _token_method, token = t + return self._method_name, token + + +class Saml2ScopedToken(v3.Token): + """Class for scoping unscoped saml2 token.""" + + _auth_method_class = Saml2ScopedTokenMethod + + def __init__(self, auth_url, token, **kwargs): + super(Saml2ScopedToken, self).__init__(auth_url, token, **kwargs) + if not (self.project_id or self.domain_id): + raise exceptions.ValidationError( + _('Neither project nor domain specified')) diff --git a/keystoneclient/contrib/bootstrap/shell.py b/keystoneclient/contrib/bootstrap/shell.py deleted file mode 100644 index e6bdc514e..000000000 --- a/keystoneclient/contrib/bootstrap/shell.py +++ /dev/null @@ -1,28 +0,0 @@ -from keystoneclient import utils -from keystoneclient.v2_0 import client - - -@utils.arg('--user-name', metavar='', default='admin', dest='user', - help='The name of the user to be created (default="admin").') -@utils.arg('--pass', metavar='', required=True, dest='passwd', - help='The password for the new user.') -@utils.arg('--role-name', metavar='', default='admin', dest='role', - help='The name of the role to be created and granted to the user ' - '(default="admin").') -@utils.arg('--tenant-name', metavar='', default='admin', - dest='tenant', - help='The name of the tenant to be created (default="admin").') -def do_bootstrap(kc, args): - """Grants a new role to a new user on a new tenant, after creating each.""" - tenant = kc.tenants.create(tenant_name=args.tenant) - role = kc.roles.create(name=args.role) - user = kc.users.create(name=args.user, password=args.passwd, email=None) - kc.roles.add_user_role(user=user, role=role, tenant=tenant) - - # verify the result - user_client = client.Client( - username=args.user, - password=args.passwd, - tenant_name=args.tenant, - auth_url=kc.management_url) - user_client.authenticate() diff --git a/keystoneclient/contrib/ec2/utils.py b/keystoneclient/contrib/ec2/utils.py index fcd8ee379..f7cefa1bf 100644 --- a/keystoneclient/contrib/ec2/utils.py +++ b/keystoneclient/contrib/ec2/utils.py @@ -1,6 +1,4 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC +# Copyright 2012 OpenStack Foundation # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2011 - 2012 Justin Santa Barbara @@ -21,61 +19,127 @@ import base64 import hashlib import hmac -import urllib +import re +import urllib.parse + +from keystoneclient.i18n import _ class Ec2Signer(object): - """ - Utility class which adds allows a request to be signed with an AWS style - signature, which can then be used for authentication via the keystone ec2 - authentication extension + """Utility class for EC2 signing of request. + + This allows a request to be signed with an AWS style signature, + which can then be used for authentication via the keystone ec2 + authentication extension. """ def __init__(self, secret_key): - secret_key = secret_key.encode() - self.hmac = hmac.new(secret_key, digestmod=hashlib.sha1) + self.secret_key = secret_key.encode() + self.hmac = hmac.new(self.secret_key, digestmod=hashlib.sha1) if hashlib.sha256: - self.hmac_256 = hmac.new(secret_key, digestmod=hashlib.sha256) + self.hmac_256 = hmac.new(self.secret_key, digestmod=hashlib.sha256) + + def _v4_creds(self, credentials): + """Detect if the credentials are for a v4 signed request. + + Check is needed since AWS removed the SignatureVersion field from + the v4 request spec... + + This expects a dict of the request headers to be passed in the + credentials dict, since the recommended way to pass v4 creds is + via the 'Authorization' header + see http://docs.aws.amazon.com/general/latest/gr/ + sigv4-signed-request-examples.html + + Alternatively X-Amz-Algorithm can be specified as a query parameter, + and the authentication data can also passed as query parameters. + + Note a hash of the request body is also required in the credentials + for v4 auth to work in the body_hash key, calculated via: + hashlib.sha256(req.body).hexdigest() + """ + try: + auth_str = credentials['headers']['Authorization'] + if auth_str.startswith('AWS4-HMAC-SHA256'): + return True + except KeyError: + # Alternatively the Authorization data can be passed via + # the query params list, check X-Amz-Algorithm=AWS4-HMAC-SHA256 + try: + if (credentials['params']['X-Amz-Algorithm'] == + 'AWS4-HMAC-SHA256'): + return True + except KeyError: # nosec(cjschaef): in cases of not finding + # entries, simply return False + pass + + return False def generate(self, credentials): """Generate auth string according to what SignatureVersion is given.""" - if credentials['params']['SignatureVersion'] == '0': + signature_version = credentials['params'].get('SignatureVersion') + if signature_version == '0': return self._calc_signature_0(credentials['params']) - if credentials['params']['SignatureVersion'] == '1': + if signature_version == '1': return self._calc_signature_1(credentials['params']) - if credentials['params']['SignatureVersion'] == '2': + if signature_version == '2': return self._calc_signature_2(credentials['params'], credentials['verb'], credentials['host'], credentials['path']) - raise Exception('Unknown Signature Version: %s' % - credentials['params']['SignatureVersion']) + if self._v4_creds(credentials): + return self._calc_signature_4(credentials['params'], + credentials['verb'], + credentials['host'], + credentials['path'], + credentials['headers'], + credentials['body_hash']) + + if signature_version is not None: + raise Exception(_('Unknown signature version: %s') % + signature_version) + else: + raise Exception(_('Unexpected signature format')) @staticmethod def _get_utf8_value(value): """Get the UTF8-encoded version of a value.""" - if not isinstance(value, str) and not isinstance(value, unicode): + if not isinstance(value, (str, bytes)): value = str(value) - if isinstance(value, unicode): + if isinstance(value, str): return value.encode('utf-8') else: return value def _calc_signature_0(self, params): """Generate AWS signature version 0 string.""" - s = params['Action'] + params['Timestamp'] + s = (params['Action'] + params['Timestamp']).encode('utf-8') self.hmac.update(s) - return base64.b64encode(self.hmac.digest()) + return base64.b64encode(self.hmac.digest()).decode('utf-8') def _calc_signature_1(self, params): """Generate AWS signature version 1 string.""" - keys = params.keys() - keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) - for key in keys: - self.hmac.update(key) + for key in sorted(params, key=str.lower): + self.hmac.update(key.encode('utf-8')) val = self._get_utf8_value(params[key]) self.hmac.update(val) - return base64.b64encode(self.hmac.digest()) + return base64.b64encode(self.hmac.digest()).decode('utf-8') + + @staticmethod + def _canonical_qs(params): + """Construct a sorted, correctly encoded query string. + + This is required for _calc_signature_2 and _calc_signature_4. + """ + keys = list(params) + keys.sort() + pairs = [] + for key in keys: + val = Ec2Signer._get_utf8_value(params[key]) + val = urllib.parse.quote(val, safe='-_~') + pairs.append(urllib.parse.quote(key, safe='') + '=' + val) + qs = '&'.join(pairs) + return qs def _calc_signature_2(self, params, verb, server_string, path): """Generate AWS signature version 2 string.""" @@ -86,15 +150,134 @@ def _calc_signature_2(self, params, verb, server_string, path): else: current_hmac = self.hmac params['SignatureMethod'] = 'HmacSHA1' - keys = params.keys() - keys.sort() - pairs = [] - for key in keys: - val = self._get_utf8_value(params[key]) - val = urllib.quote(val, safe='-_~') - pairs.append(urllib.quote(key, safe='') + '=' + val) - qs = '&'.join(pairs) - string_to_sign += qs - current_hmac.update(string_to_sign) - b64 = base64.b64encode(current_hmac.digest()) + string_to_sign += self._canonical_qs(params) + current_hmac.update(string_to_sign.encode('utf-8')) + b64 = base64.b64encode(current_hmac.digest()).decode('utf-8') return b64 + + def _calc_signature_4(self, params, verb, server_string, path, headers, + body_hash): + """Generate AWS signature version 4 string.""" + def sign(key, msg): + return hmac.new(key, self._get_utf8_value(msg), + hashlib.sha256).digest() + + def signature_key(datestamp, region_name, service_name): + """Signature key derivation. + + See http://docs.aws.amazon.com/general/latest/gr/ + signature-v4-examples.html#signature-v4-examples-python + """ + k_date = sign(self._get_utf8_value(b"AWS4" + self.secret_key), + datestamp) + k_region = sign(k_date, region_name) + k_service = sign(k_region, service_name) + k_signing = sign(k_service, "aws4_request") + return k_signing + + def auth_param(param_name): + """Get specified auth parameter. + + Provided via one of: + - the Authorization header + - the X-Amz-* query parameters + """ + try: + auth_str = headers['Authorization'] + param_str = auth_str.partition( + '%s=' % param_name)[2].split(',')[0] + except KeyError: + param_str = params.get('X-Amz-%s' % param_name) + return param_str + + def date_param(): + """Get the X-Amz-Date' value. + + The value can be either a header or parameter. + + Note AWS supports parsing the Date header also, but this is not + currently supported here as it will require some format mangling + So the X-Amz-Date value must be YYYYMMDDTHHMMSSZ format, then it + can be used to match against the YYYYMMDD format provided in the + credential scope. + see: + http://docs.aws.amazon.com/general/latest/gr/ + sigv4-date-handling.html + """ + try: + return headers['X-Amz-Date'] + except KeyError: + return params.get('X-Amz-Date') + + def canonical_header_str(): + # Get the list of headers to include, from either + # - the Authorization header (SignedHeaders key) + # - the X-Amz-SignedHeaders query parameter + headers_lower = dict((k.lower().strip(), v.strip()) + for (k, v) in headers.items()) + + # Boto versions < 2.9.3 strip the port component of the host:port + # header, so detect the user-agent via the header and strip the + # port if we detect an old boto version. FIXME: remove when all + # distros package boto >= 2.9.3, this is a transitional workaround + user_agent = headers_lower.get('user-agent', '') + strip_port = re.match(r'Boto/2\.[0-9]\.[0-2]', user_agent) + + header_list = [] + sh_str = auth_param('SignedHeaders') + for h in sh_str.split(';'): + if h not in headers_lower: + continue + + if h == 'host' and strip_port: + header_list.append('%s:%s' % + (h, headers_lower[h].split(':')[0])) + continue + + header_list.append('%s:%s' % (h, headers_lower[h])) + return '\n'.join(header_list) + '\n' + + def canonical_query_str(verb, params): + # POST requests pass parameters in through the request body + canonical_qs = '' + if verb.upper() != 'POST': + canonical_qs = self._canonical_qs(params) + return canonical_qs + + # Create canonical request: + # http://docs.aws.amazon.com/general/latest/gr/ + # sigv4-create-canonical-request.html + # Get parameters and headers in expected string format + cr = "\n".join((verb.upper(), path, + canonical_query_str(verb, params), + canonical_header_str(), + auth_param('SignedHeaders'), + body_hash)) + + # Check the date, reject any request where the X-Amz-Date doesn't + # match the credential scope + credential = auth_param('Credential') + credential_split = credential.split('/') + credential_scope = '/'.join(credential_split[1:]) + credential_date = credential_split[1] + param_date = date_param() + if not param_date.startswith(credential_date): + raise Exception(_('Request date mismatch error')) + + # Create the string to sign + # http://docs.aws.amazon.com/general/latest/gr/ + # sigv4-create-string-to-sign.html + cr = cr.encode('utf-8') + string_to_sign = '\n'.join(('AWS4-HMAC-SHA256', + param_date, + credential_scope, + hashlib.sha256(cr).hexdigest())) + + # Calculate the derived key, this requires a datestamp, region + # and service, which can be extracted from the credential scope + (req_region, req_service) = credential_split[2:4] + s_key = signature_key(credential_date, req_region, req_service) + # Finally calculate the signature! + signature = hmac.new(s_key, self._get_utf8_value(string_to_sign), + hashlib.sha256).hexdigest() + return signature diff --git a/keystoneclient/discover.py b/keystoneclient/discover.py new file mode 100644 index 000000000..16174162d --- /dev/null +++ b/keystoneclient/discover.py @@ -0,0 +1,362 @@ +# 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 warnings + +from debtcollector import removals +from keystoneauth1 import plugin + +from keystoneclient import _discover +from keystoneclient import exceptions +from keystoneclient.i18n import _ +from keystoneclient import session as client_session +from keystoneclient.v2_0 import client as v2_client +from keystoneclient.v3 import client as v3_client + + +_CLIENT_VERSIONS = {2: v2_client.Client, + 3: v3_client.Client} + + +# functions needed from the private file that can be made public + +def normalize_version_number(version): + """Turn a version representation into a tuple. + + Takes a string, tuple or float which represent version formats we can + handle and converts them into a (major, minor) version tuple that we can + actually use for discovery. + + e.g. 'v3.3' gives (3, 3) + 3.1 gives (3, 1) + + :param version: Inputted version number to try and convert. + + :returns: A usable version tuple + :rtype: tuple + + :raises TypeError: if the inputted version cannot be converted to tuple. + """ + return _discover.normalize_version_number(version) + + +def version_match(required, candidate): + """Test that an available version satisfies the required version. + + To be suitable a version must be of the same major version as required + and be at least a match in minor/patch level. + + eg. 3.3 is a match for a required 3.1 but 4.1 is not. + + :param tuple required: the version that must be met. + :param tuple candidate: the version to test against required. + + :returns: True if candidate is suitable False otherwise. + :rtype: bool + """ + return _discover.version_match(required, candidate) + + +def available_versions(url, session=None, **kwargs): + """Retrieve raw version data from a url.""" + if not session: + session = client_session.Session._construct(kwargs) + + return _discover.get_version_data(session, url) + + +class Discover(_discover.Discover): + """A means to discover and create clients. + + Clients are created depending on the supported API versions on the server. + + Querying the server is done on object creation and every subsequent method + operates upon the data that was retrieved. + + The connection parameters associated with this method are the same format + and name as those used by a client (see + :py:class:`keystoneclient.v2_0.client.Client` and + :py:class:`keystoneclient.v3.client.Client`). If not overridden in + subsequent methods they will also be what is passed to the constructed + client. + + In the event that auth_url and endpoint is provided then auth_url will be + used in accordance with how the client operates. + + .. warning:: + + Creating an instance of this class without using the session argument + is deprecated as of the 1.7.0 release and may be removed in the 2.0.0 + release. + + :param session: A session object that will be used for communication. + Clients will also be constructed with this session. + :type session: keystoneclient.session.Session + :param string auth_url: Identity service endpoint for authorization. + (optional) + :param string endpoint: A user-supplied endpoint URL for the identity + service. (optional) + :param string original_ip: The original IP of the requesting user which + will be sent to identity service in a + 'Forwarded' header. (optional) This is ignored + if a session is provided. Deprecated as of the + 1.7.0 release and may be removed in the 2.0.0 + release. + :param boolean debug: Enables debug logging of all request and responses to + the identity service. default False (optional) + This is ignored if a session is provided. Deprecated + as of the 1.7.0 release and may be removed in the + 2.0.0 release. + :param string cacert: Path to the Privacy Enhanced Mail (PEM) file which + contains the trusted authority X.509 certificates + needed to established SSL connection with the + identity service. (optional) This is ignored if a + session is provided. Deprecated as of the 1.7.0 + release and may be removed in the 2.0.0 release. + :param string key: Path to the Privacy Enhanced Mail (PEM) file which + contains the unencrypted client private key needed to + established two-way SSL connection with the identity + service. (optional) This is ignored if a session is + provided. Deprecated as of the 1.7.0 release and may be + removed in the 2.0.0 release. + :param string cert: Path to the Privacy Enhanced Mail (PEM) file which + contains the corresponding X.509 client certificate + needed to established two-way SSL connection with the + identity service. (optional) This is ignored if a + session is provided. Deprecated as of the 1.7.0 release + and may be removed in the 2.0.0 release. + :param boolean insecure: Does not perform X.509 certificate validation when + establishing SSL connection with identity service. + default: False (optional) This is ignored if a + session is provided. Deprecated as of the 1.7.0 + release and may be removed in the 2.0.0 release. + :param bool authenticated: Should a token be used to perform the initial + discovery operations. default: None (attach a + token if an auth plugin is available). + + """ + + def __init__(self, session=None, authenticated=None, **kwargs): + if not session: + warnings.warn( + 'Constructing a Discover instance without using a session is ' + 'deprecated as of the 1.7.0 release and may be removed in the ' + '2.0.0 release.', DeprecationWarning) + session = client_session.Session._construct(kwargs) + kwargs['session'] = session + + url = None + endpoint = kwargs.pop('endpoint', None) + auth_url = kwargs.pop('auth_url', None) + + if endpoint: + self._use_endpoint = True + url = endpoint + elif auth_url: + self._use_endpoint = False + url = auth_url + elif session.auth: + self._use_endpoint = False + url = session.get_endpoint(interface=plugin.AUTH_INTERFACE) + + if not url: + raise exceptions.DiscoveryFailure( + _('Not enough information to determine URL. Provide' + ' either a Session, or auth_url or endpoint')) + + self._client_kwargs = kwargs + super(Discover, self).__init__(session, url, + authenticated=authenticated) + + @removals.remove(message='Use raw_version_data instead.', version='1.7.0', + removal_version='2.0.0') + def available_versions(self, **kwargs): + """Return a list of identity APIs available on the server. + + The list returned includes the data associated with them. + + .. warning:: + + This method is deprecated as of the 1.7.0 release in favor of + :meth:`raw_version_data` and may be removed in the 2.0.0 release. + + :param bool unstable: Accept endpoints not marked 'stable'. (optional) + Equates to setting allow_experimental + and allow_unknown to True. + :param bool allow_experimental: Allow experimental version endpoints. + :param bool allow_deprecated: Allow deprecated version endpoints. + :param bool allow_unknown: Allow endpoints with an unrecognised status. + + :returns: A List of dictionaries as presented by the server. Each dict + will contain the version and the URL to use for the version. + It is a direct representation of the layout presented by the + identity API. + """ + return self.raw_version_data(**kwargs) + + @removals.removed_kwarg( + 'unstable', + message='Use allow_experimental and allow_unknown instead.', + version='1.7.0', removal_version='2.0.0') + def raw_version_data(self, unstable=False, **kwargs): + """Get raw version information from URL. + + Raw data indicates that only minimal validation processing is performed + on the data, so what is returned here will be the data in the same + format it was received from the endpoint. + + :param bool unstable: equates to setting allow_experimental and + allow_unknown. This argument is deprecated as of + the 1.7.0 release and may be removed in the 2.0.0 + release. + :param bool allow_experimental: Allow experimental version endpoints. + :param bool allow_deprecated: Allow deprecated version endpoints. + :param bool allow_unknown: Allow endpoints with an unrecognised status. + + :returns: The endpoints returned from the server that match the + criteria. + :rtype: List + + Example:: + + >>> from keystoneclient import discover + >>> disc = discover.Discovery(auth_url='http://localhost:5000') + >>> disc.raw_version_data() + [{'id': 'v3.0', + 'links': [{'href': 'http://127.0.0.1:5000/v3/', + 'rel': 'self'}], + 'media-types': [ + {'base': 'application/json', + 'type': 'application/vnd.openstack.identity-v3+json'}, + {'base': 'application/xml', + 'type': 'application/vnd.openstack.identity-v3+xml'}], + 'status': 'stable', + 'updated': '2013-03-06T00:00:00Z'}, + {'id': 'v2.0', + 'links': [{'href': 'http://127.0.0.1:5000/v2.0/', + 'rel': 'self'}, + {'href': '...', + 'rel': 'describedby', + 'type': 'application/pdf'}], + 'media-types': [ + {'base': 'application/json', + 'type': 'application/vnd.openstack.identity-v2.0+json'}, + {'base': 'application/xml', + 'type': 'application/vnd.openstack.identity-v2.0+xml'}], + 'status': 'stable', + 'updated': '2013-03-06T00:00:00Z'}] + """ + if unstable: + kwargs.setdefault('allow_experimental', True) + kwargs.setdefault('allow_unknown', True) + + return super(Discover, self).raw_version_data(**kwargs) + + def _calculate_version(self, version, unstable): + version_data = None + + if version: + version_data = self.data_for(version) + else: + # if no version specified pick the latest one + all_versions = self.version_data(unstable=unstable) + if all_versions: + version_data = all_versions[-1] + + if not version_data: + msg = _('Could not find a suitable endpoint') + + if version: + msg = _('Could not find a suitable endpoint for client ' + 'version: %s') % str(version) + + raise exceptions.VersionNotAvailable(msg) + + return version_data + + def _create_client(self, version_data, **kwargs): + # Get the client for the version requested that was returned + try: + client_class = _CLIENT_VERSIONS[version_data['version'][0]] + except KeyError: + version = '.'.join(str(v) for v in version_data['version']) + msg = _('No client available for version: %s') % version + raise exceptions.DiscoveryFailure(msg) + + # kwargs should take priority over stored kwargs. + for k, v in self._client_kwargs.items(): + kwargs.setdefault(k, v) + + # restore the url to either auth_url or endpoint depending on what + # was initially given + if self._use_endpoint: + kwargs['auth_url'] = None + kwargs['endpoint'] = version_data['url'] + else: + kwargs['auth_url'] = version_data['url'] + kwargs['endpoint'] = None + + return client_class(**kwargs) + + def create_client(self, version=None, unstable=False, **kwargs): + """Factory function to create a new identity service client. + + :param tuple version: The required version of the identity API. If + specified the client will be selected such that + the major version is equivalent and an endpoint + provides at least the specified minor version. + For example to specify the 3.1 API use (3, 1). + (optional) + :param bool unstable: Accept endpoints not marked 'stable'. (optional) + :param kwargs: Additional arguments will override those provided to + this object's constructor. + + :returns: An instantiated identity client object. + + :raises keystoneclient.exceptions.DiscoveryFailure: if the server + response is invalid + :raises keystoneclient.exceptions.VersionNotAvailable: if a suitable + client cannot be found. + """ + version_data = self._calculate_version(version, unstable) + return self._create_client(version_data, **kwargs) + + +def add_catalog_discover_hack(service_type, old, new): + """Add a version removal rule for a particular service. + + Originally deployments of OpenStack would contain a versioned endpoint in + the catalog for different services. E.g. an identity service might look + like ``http://localhost:5000/v2.0``. This is a problem when we want to use + a different version like v3.0 as there is no way to tell where it is + located. We cannot simply change all service catalogs either so there must + be a way to handle the older style of catalog. + + This function adds a rule for a given service type that if part of the URL + matches a given regular expression in *old* then it will be replaced with + the *new* value. This will replace all instances of old with new. It should + therefore contain a regex anchor. + + For example the included rule states:: + + add_catalog_version_hack('identity', re.compile('/v2.0/?$'), '/') + + so if the catalog retrieves an *identity* URL that ends with /v2.0 or + /v2.0/ then it should replace it simply with / to fix the user's catalog. + + :param str service_type: The service type as defined in the catalog that + the rule will apply to. + :param re.RegexObject old: The regular expression to search for and replace + if found. + :param str new: The new string to replace the pattern with. + """ + _discover._VERSION_HACKS.add_discover_hack(service_type, old, new) diff --git a/keystoneclient/exceptions.py b/keystoneclient/exceptions.py index 213f0781c..034e5c977 100644 --- a/keystoneclient/exceptions.py +++ b/keystoneclient/exceptions.py @@ -1,154 +1,439 @@ # Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Nebula, Inc. +# +# 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. +"""Exception definitions.""" + +from keystoneauth1 import exceptions as _exc + +from keystoneclient.i18n import _ + + +ClientException = _exc.ClientException +"""The base exception class for all exceptions this library raises. + +An alias of :py:exc:`keystoneauth1.exceptions.base.ClientException` """ -Exception definitions. + +ConnectionError = _exc.ConnectionError +"""Cannot connect to API service. + +An alias of :py:exc:`keystoneauth1.exceptions.connection.ConnectionError` """ +ConnectionRefused = _exc.ConnectFailure +"""Connection refused while trying to connect to API service. -class CommandError(Exception): - pass +An alias of :py:exc:`keystoneauth1.exceptions.connection.ConnectFailure` +""" +SSLError = _exc.SSLError +"""An SSL error occurred. + +An alias of :py:exc:`keystoneauth1.exceptions.connection.SSLError` +""" + +AuthorizationFailure = _exc.AuthorizationFailure +"""Cannot authorize API client. + +An alias of :py:exc:`keystoneauth1.exceptions.auth.AuthorizationFailure` +""" -class ValidationError(Exception): - pass +class ValidationError(ClientException): + """Error in validation on API client side.""" -class AuthorizationFailure(Exception): pass -class NoTokenLookupException(Exception): - """This form of authentication does not support looking up - endpoints from an existing token.""" +class UnsupportedVersion(ClientException): + """User is trying to use an unsupported version of the API.""" + pass -class EndpointNotFound(Exception): - """Could not find Service or Region in Service Catalog.""" +class CommandError(ClientException): + """Error in CLI tool.""" + pass -class EmptyCatalog(Exception): - """ The service catalog is empty. """ +class AuthPluginOptionsMissing(AuthorizationFailure): + """Auth plugin misses some options.""" + + def __init__(self, opt_names): + super(AuthPluginOptionsMissing, self).__init__( + _("Authentication failed. Missing options: %s") % + ", ".join(opt_names)) + self.opt_names = opt_names + + +class AuthSystemNotFound(AuthorizationFailure): + """User has specified an AuthSystem that is not installed.""" + + def __init__(self, auth_system): + super(AuthSystemNotFound, self).__init__( + _("AuthSystemNotFound: %r") % auth_system) + self.auth_system = auth_system + + +class NoUniqueMatch(ClientException): + """Multiple entities found instead of one.""" + pass -class ClientException(Exception): - """ - The base exception class for all exceptions this library raises. - """ - def __init__(self, code, message=None, details=None): - self.code = code - self.message = message or self.__class__.message - self.details = details +EndpointException = _exc.CatalogException +"""Something is rotten in Service Catalog. - def __str__(self): - return "%s (HTTP %s)" % (self.message, self.code) +An alias of :py:exc:`keystoneauth1.exceptions.catalog.CatalogException` +""" +EndpointNotFound = _exc.EndpointNotFound +"""Could not find requested endpoint in Service Catalog. -class BadRequest(ClientException): - """ - HTTP 400 - Bad request: you sent some malformed data. - """ - http_status = 400 - message = "Bad request" +An alias of :py:exc:`keystoneauth1.exceptions.catalog.EndpointNotFound` +""" -class Unauthorized(ClientException): - """ - HTTP 401 - Unauthorized: bad credentials. - """ - http_status = 401 - message = "Unauthorized" +class AmbiguousEndpoints(EndpointException): + """Found more than one matching endpoint in Service Catalog.""" + def __init__(self, endpoints=None): + super(AmbiguousEndpoints, self).__init__( + _("AmbiguousEndpoints: %r") % endpoints) + self.endpoints = endpoints -class Forbidden(ClientException): - """ - HTTP 403 - Forbidden: your credentials don't give you access to this - resource. - """ - http_status = 403 - message = "Forbidden" +HttpError = _exc.HttpError +"""The base exception class for all HTTP exceptions. -class NotFound(ClientException): - """ - HTTP 404 - Not found - """ - http_status = 404 - message = "Not found" +An alias of :py:exc:`keystoneauth1.exceptions.http.HttpError` +""" +HTTPClientError = _exc.HTTPClientError +"""Client-side HTTP error. -class Conflict(ClientException): - """ - HTTP 409 - Conflict - """ - http_status = 409 - message = "Conflict" +Exception for cases in which the client seems to have erred. +An alias of :py:exc:`keystoneauth1.exceptions.http.HTTPClientError` +""" +HttpServerError = _exc.HttpServerError +"""Server-side HTTP error. -class OverLimit(ClientException): - """ - HTTP 413 - Over limit: you're over the API limits for this time period. - """ - http_status = 413 - message = "Over limit" +Exception for cases in which the server is aware that it has +erred or is incapable of performing the request. +An alias of :py:exc:`keystoneauth1.exceptions.http.HttpServerError` +""" -# NotImplemented is a python keyword. -class HTTPNotImplemented(ClientException): - """ - HTTP 501 - Not Implemented: the server does not support this operation. - """ - http_status = 501 - message = "Not Implemented" +class HTTPRedirection(HttpError): + """HTTP Redirection.""" + message = _("HTTP Redirection") -class ServiceUnavailable(ClientException): - """ - HTTP 503 - Service Unavailable: The server is currently unavailable. + +class MultipleChoices(HTTPRedirection): + """HTTP 300 - Multiple Choices. + + Indicates multiple options for the resource that the client may follow. """ - http_status = 503 - message = "Service Unavailable" + http_status = 300 + message = _("Multiple Choices") -# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() -# so we can do this: -# _code_map = dict((c.http_status, c) -# for c in ClientException.__subclasses__()) -# -# Instead, we have to hardcode it: -_code_map = dict((c.http_status, c) for c in [BadRequest, - Unauthorized, - Forbidden, - NotFound, - OverLimit, - HTTPNotImplemented, - ServiceUnavailable]) +BadRequest = _exc.BadRequest +"""HTTP 400 - Bad Request. -def from_response(response, body): - """ - Return an instance of an ClientException or subclass - based on an requests response. +The request cannot be fulfilled due to bad syntax. +An alias of :py:exc:`keystoneauth1.exceptions.http.BadRequest` +""" + +Unauthorized = _exc.Unauthorized +"""HTTP 401 - Unauthorized. + +Similar to 403 Forbidden, but specifically for use when authentication +is required and has failed or has not yet been provided. +An alias of :py:exc:`keystoneauth1.exceptions.http.Unauthorized` +""" + +PaymentRequired = _exc.PaymentRequired +"""HTTP 402 - Payment Required. + +Reserved for future use. +An alias of :py:exc:`keystoneauth1.exceptions.http.PaymentRequired` +""" + +Forbidden = _exc.Forbidden +"""HTTP 403 - Forbidden. + +The request was a valid request, but the server is refusing to respond +to it. +An alias of :py:exc:`keystoneauth1.exceptions.http.Forbidden` +""" + +NotFound = _exc.NotFound +"""HTTP 404 - Not Found. + +The requested resource could not be found but may be available again +in the future. +An alias of :py:exc:`keystoneauth1.exceptions.http.NotFound` +""" + +MethodNotAllowed = _exc.MethodNotAllowed +"""HTTP 405 - Method Not Allowed. + +A request was made of a resource using a request method not supported +by that resource. +An alias of :py:exc:`keystoneauth1.exceptions.http.MethodNotAllowed` +""" + +NotAcceptable = _exc.NotAcceptable +"""HTTP 406 - Not Acceptable. + +The requested resource is only capable of generating content not +acceptable according to the Accept headers sent in the request. +An alias of :py:exc:`keystoneauth1.exceptions.http.NotAcceptable` +""" + +ProxyAuthenticationRequired = _exc.ProxyAuthenticationRequired +"""HTTP 407 - Proxy Authentication Required. + +The client must first authenticate itself with the proxy. +An alias of :py:exc:`keystoneauth1.exceptions.http.ProxyAuthenticationRequired` +""" + +RequestTimeout = _exc.RequestTimeout +"""HTTP 408 - Request Timeout. + +The server timed out waiting for the request. +An alias of :py:exc:`keystoneauth1.exceptions.http.RequestTimeout` +""" + +Conflict = _exc.Conflict +"""HTTP 409 - Conflict. + +Indicates that the request could not be processed because of conflict +in the request, such as an edit conflict. +An alias of :py:exc:`keystoneauth1.exceptions.http.Conflict` +""" + +Gone = _exc.Gone +"""HTTP 410 - Gone. - Usage:: +Indicates that the resource requested is no longer available and will +not be available again. +An alias of :py:exc:`keystoneauth1.exceptions.http.Gone` +""" + +LengthRequired = _exc.LengthRequired +"""HTTP 411 - Length Required. + +The request did not specify the length of its content, which is +required by the requested resource. +An alias of :py:exc:`keystoneauth1.exceptions.http.LengthRequired` +""" + +PreconditionFailed = _exc.PreconditionFailed +"""HTTP 412 - Precondition Failed. + +The server does not meet one of the preconditions that the requester +put on the request. +An alias of :py:exc:`keystoneauth1.exceptions.http.PreconditionFailed` +""" + +RequestEntityTooLarge = _exc.RequestEntityTooLarge +"""HTTP 413 - Request Entity Too Large. + +The request is larger than the server is willing or able to process. +An alias of :py:exc:`keystoneauth1.exceptions.http.RequestEntityTooLarge` +""" + +RequestUriTooLong = _exc.RequestUriTooLong +"""HTTP 414 - Request-URI Too Long. + +The URI provided was too long for the server to process. +An alias of :py:exc:`keystoneauth1.exceptions.http.RequestUriTooLong` +""" + +UnsupportedMediaType = _exc.UnsupportedMediaType +"""HTTP 415 - Unsupported Media Type. + +The request entity has a media type which the server or resource does +not support. +An alias of :py:exc:`keystoneauth1.exceptions.http.UnsupportedMediaType` +""" + +RequestedRangeNotSatisfiable = _exc.RequestedRangeNotSatisfiable +"""HTTP 416 - Requested Range Not Satisfiable. + +The client has asked for a portion of the file, but the server cannot +supply that portion. +An alias of +:py:exc:`keystoneauth1.exceptions.http.RequestedRangeNotSatisfiable` +""" + +ExpectationFailed = _exc.ExpectationFailed +"""HTTP 417 - Expectation Failed. + +The server cannot meet the requirements of the Expect request-header field. +An alias of :py:exc:`keystoneauth1.exceptions.http.ExpectationFailed` +""" + +UnprocessableEntity = _exc.UnprocessableEntity +"""HTTP 422 - Unprocessable Entity. + +The request was well-formed but was unable to be followed due to semantic +errors. +An alias of :py:exc:`keystoneauth1.exceptions.http.UnprocessableEntity` +""" + +InternalServerError = _exc.InternalServerError +"""HTTP 500 - Internal Server Error. + +A generic error message, given when no more specific message is suitable. +An alias of :py:exc:`keystoneauth1.exceptions.http.InternalServerError` +""" + +HttpNotImplemented = _exc.HttpNotImplemented +"""HTTP 501 - Not Implemented. + +The server either does not recognize the request method, or it lacks +the ability to fulfill the request. +An alias of :py:exc:`keystoneauth1.exceptions.http.HttpNotImplemented` +""" + +BadGateway = _exc.BadGateway +"""HTTP 502 - Bad Gateway. + +The server was acting as a gateway or proxy and received an invalid +response from the upstream server. +An alias of :py:exc:`keystoneauth1.exceptions.http.BadGateway` +""" + +ServiceUnavailable = _exc.ServiceUnavailable +"""HTTP 503 - Service Unavailable. + +The server is currently unavailable. +An alias of :py:exc:`keystoneauth1.exceptions.http.ServiceUnavailable` +""" + +GatewayTimeout = _exc.GatewayTimeout +"""HTTP 504 - Gateway Timeout. + +The server was acting as a gateway or proxy and did not receive a timely +response from the upstream server. +An alias of :py:exc:`keystoneauth1.exceptions.http.GatewayTimeout` +""" + +HttpVersionNotSupported = _exc.HttpVersionNotSupported +"""HTTP 505 - HttpVersion Not Supported. - resp = requests.request(...) - if resp.status_code != 200: - raise exception_from_response(resp, resp.text) +The server does not support the HTTP protocol version used in the request. +An alias of :py:exc:`keystoneauth1.exceptions.http.HttpVersionNotSupported` +""" + +from_response = _exc.from_response +"""Return an instance of :class:`HttpError` or subclass based on response. + +An alias of :py:func:`keystoneauth1.exceptions.http.from_response` +""" + + +# NOTE(akurilin): This alias should be left here to support backwards +# compatibility until we are sure that usage of these exceptions in +# projects is correct. +HTTPNotImplemented = HttpNotImplemented +Timeout = RequestTimeout +HTTPError = HttpError + + +class CertificateConfigError(Exception): + """Error reading the certificate.""" + + def __init__(self, output): + self.output = output + msg = _('Unable to load certificate.') + super(CertificateConfigError, self).__init__(msg) + + +class CMSError(Exception): + """Error reading the certificate.""" + + def __init__(self, output): + self.output = output + msg = _('Unable to sign or verify data.') + super(CMSError, self).__init__(msg) + + +EmptyCatalog = _exc.EmptyCatalog +"""The service catalog is empty. + +An alias of :py:exc:`keystoneauth1.exceptions.catalog.EmptyCatalog` +""" + +DiscoveryFailure = _exc.DiscoveryFailure +"""Discovery of client versions failed. + +An alias of :py:exc:`keystoneauth1.exceptions.discovery.DiscoveryFailure` +""" + +VersionNotAvailable = _exc.VersionNotAvailable +"""Discovery failed as the version you requested is not available. + +An alias of :py:exc:`keystoneauth1.exceptions.discovery.VersionNotAvailable` +""" + + +class MethodNotImplemented(ClientException): + """Method not implemented by the keystoneclient API.""" + + +MissingAuthPlugin = _exc.MissingAuthPlugin +"""An authenticated request is required but no plugin available. + +An alias of :py:exc:`keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin` +""" + +NoMatchingPlugin = _exc.NoMatchingPlugin +"""There were no auth plugins that could be created from the parameters +provided. + +An alias of :py:exc:`keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin` +""" + + +class UnsupportedParameters(ClientException): + """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. """ - cls = _code_map.get(response.status_code, ClientException) - if body: - if hasattr(body, 'keys'): - error = body[body.keys()[0]] - message = error.get('message', None) - details = error.get('details', None) - else: - # If we didn't get back a properly formed error message we - # probably couldn't communicate with Keystone at all. - message = "Unable to communicate with identity service: %s." % body - details = None - return cls(code=response.status_code, message=message, details=details) - else: - return cls(code=response.status_code) + + 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 InvalidResponse(ClientException): + """The response from the server is not valid for this request.""" + + def __init__(self, response): + super(InvalidResponse, self).__init__() + self.response = response diff --git a/keystoneclient/fixture/__init__.py b/keystoneclient/fixture/__init__.py new file mode 100644 index 000000000..92034a0da --- /dev/null +++ b/keystoneclient/fixture/__init__.py @@ -0,0 +1,58 @@ +# 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 testing. + +The generators in this directory produce keystone compliant structures for +use in testing. +They should be considered part of the public API because they may be relied +upon to generate test tokens for other clients. However they should never be +imported into the main client (keystoneclient or other). Because of this there +may be dependencies from this module on libraries that are only available in +testing. + +.. warning:: + + The keystoneclient.fixture package is deprecated in favor of + keystoneauth1.fixture and will not be supported. + +""" + +# flake8: noqa: F405 + +import warnings + +from keystoneclient.fixture.discovery import * # noqa +from keystoneclient.fixture import exception +from keystoneclient.fixture import v2 +from keystoneclient.fixture import v3 + + +warnings.warn( + "The keystoneclient.fixture package is deprecated in favor of " + "keystoneauth1.fixture and will not be supported.", DeprecationWarning) + + +FixtureValidationError = exception.FixtureValidationError +V2Token = v2.Token +V3Token = v3.Token +V3FederationToken = v3.V3FederationToken + +__all__ = ('DiscoveryList', + 'FixtureValidationError', + 'V2Discovery', + 'V3Discovery', + 'V2Token', + 'V3Token', + 'V3FederationToken', + ) diff --git a/keystoneclient/fixture/discovery.py b/keystoneclient/fixture/discovery.py new file mode 100644 index 000000000..e664d2833 --- /dev/null +++ b/keystoneclient/fixture/discovery.py @@ -0,0 +1,38 @@ +# 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.fixture import discovery + + +__all__ = ('DiscoveryList', + 'V2Discovery', + 'V3Discovery', + ) + + +V2Discovery = discovery.V2Discovery +"""A Version element for a V2 identity service endpoint. + +An alias of :py:exc:`keystoneauth1.fixture.discovery.V2Discovery` +""" + +V3Discovery = discovery.V3Discovery +"""A Version element for a V3 identity service endpoint. + +An alias of :py:exc:`keystoneauth1.fixture.discovery.V3Discovery` +""" + +DiscoveryList = discovery.DiscoveryList +"""A List of version elements. + +An alias of :py:exc:`keystoneauth1.fixture.discovery.DiscoveryList` +""" diff --git a/keystoneclient/fixture/exception.py b/keystoneclient/fixture/exception.py new file mode 100644 index 000000000..99e487633 --- /dev/null +++ b/keystoneclient/fixture/exception.py @@ -0,0 +1,20 @@ +# 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.fixture import exception + + +FixtureValidationError = exception.FixtureValidationError +"""The token you created is not legitimate. + +An alias of :py:exc:`keystoneauth1.fixture.exception.FixtureValidationError`` +""" diff --git a/keystoneclient/fixture/v2.py b/keystoneclient/fixture/v2.py new file mode 100644 index 000000000..9fbf4e005 --- /dev/null +++ b/keystoneclient/fixture/v2.py @@ -0,0 +1,20 @@ +# 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.fixture import v2 + + +Token = v2.Token +"""A V2 Keystone token that can be used for testing. + +An alias of :py:exc:`keystoneauth1.fixture.v2.Token` +""" diff --git a/keystoneclient/fixture/v3.py b/keystoneclient/fixture/v3.py new file mode 100644 index 000000000..596f3e2b5 --- /dev/null +++ b/keystoneclient/fixture/v3.py @@ -0,0 +1,26 @@ +# 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.fixture import v3 + + +Token = v3.Token +"""A V3 Keystone token that can be used for testing. + +An alias of :py:exc:`keystoneauth1.fixture.v3.Token` +""" + +V3FederationToken = v3.V3FederationToken +"""A V3 Keystone Federation token that can be used for testing. + +An alias of :py:exc:`keystoneauth1.fixture.v3.V3FederationToken` +""" diff --git a/keystoneclient/generic/__init__.py b/keystoneclient/generic/__init__.py index e69de29bb..745692105 100644 --- a/keystoneclient/generic/__init__.py +++ b/keystoneclient/generic/__init__.py @@ -0,0 +1,4 @@ + +__all__ = ( + 'client', +) diff --git a/keystoneclient/generic/client.py b/keystoneclient/generic/client.py index 4818ddf9a..7c82c195f 100644 --- a/keystoneclient/generic/client.py +++ b/keystoneclient/generic/client.py @@ -1,6 +1,4 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. +# Copyright 2010 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,15 +14,24 @@ # under the License. import logging -import urlparse +import urllib.parse as urlparse + +from debtcollector import removals -from keystoneclient import client from keystoneclient import exceptions +from keystoneclient import httpclient +from keystoneclient.i18n import _ + _logger = logging.getLogger(__name__) -class Client(client.HTTPClient): +# NOTE(jamielennox): To be removed after Pike. +@removals.removed_class('keystoneclient.generic.client.Client', + message='Use keystoneauth discovery', + version='3.9.0', + removal_version='4.0.0') +class Client(httpclient.HTTPClient): """Client for the OpenStack Keystone pre-version calls API. :param string endpoint: A user-supplied endpoint URL for the keystone @@ -47,12 +54,12 @@ class Client(client.HTTPClient): """ def __init__(self, endpoint=None, **kwargs): - """ Initialize a new client for the Keystone v2.0 API. """ + """Initialize a new client for the Keystone v2.0 API.""" super(Client, self).__init__(endpoint=endpoint, **kwargs) self.endpoint = endpoint def discover(self, url=None): - """ Discover Keystone servers and return API versions supported. + """Discover Keystone servers and return API versions supported. :param url: optional url to test (without version) @@ -74,54 +81,60 @@ def discover(self, url=None): return self._local_keystone_exists() def _local_keystone_exists(self): - """ Checks if Keystone is available on default local port 35357 """ - return self._check_keystone_versions("http://localhost:35357") + """Check if Keystone is available on default local port 35357.""" + results = self._check_keystone_versions("http://localhost:35357") + if results is None: + results = self._check_keystone_versions("https://localhost:35357") + return results def _check_keystone_versions(self, url): - """ Calls Keystone URL and detects the available API versions """ + """Call Keystone URL and detects the available API versions.""" try: - httpclient = client.HTTPClient() - resp, body = httpclient.request(url, "GET", - headers={'Accept': - 'application/json'}) - if resp.status_code in (200, 204): # some cases we get No Content + resp, body = self._request(url, "GET", + headers={'Accept': + 'application/json'}) + # Multiple Choices status code is returned by the root + # identity endpoint, with references to one or more + # Identity API versions -- v3 spec + # some cases we get No Content + if resp.status_code in (200, 204, 300): try: results = {} if 'version' in body: - results['message'] = "Keystone found at %s" % url + results['message'] = _("Keystone found at %s") % url version = body['version'] # Stable/diablo incorrect format - id, status, version_url = \ - self._get_version_info(version, url) + id, status, version_url = ( + self._get_version_info(version, url)) results[str(id)] = {"id": id, "status": status, "url": version_url} return results elif 'versions' in body: # Correct format - results['message'] = "Keystone found at %s" % url + results['message'] = _("Keystone found at %s") % url for version in body['versions']['values']: - id, status, version_url = \ - self._get_version_info(version, url) + id, status, version_url = ( + self._get_version_info(version, url)) results[str(id)] = {"id": id, "status": status, "url": version_url} return results else: - results['message'] = ("Unrecognized response from %s" - % url) + results['message'] = ( + _("Unrecognized response from %s") % url) return results except KeyError: raise exceptions.AuthorizationFailure() elif resp.status_code == 305: return self._check_keystone_versions(resp['location']) else: - raise exceptions.from_response(resp, resp.text) - except Exception as e: - _logger.exception(e) + raise exceptions.from_response(resp, "GET", url) + except Exception: + _logger.exception('Failed to detect available versions.') def discover_extensions(self, url=None): - """ Discover Keystone extensions supported. + """Discover Keystone extensions supported. :param url: optional url to test (should have a version in it) @@ -137,50 +150,36 @@ def discover_extensions(self, url=None): return self._check_keystone_extensions(url) def _check_keystone_extensions(self, url): - """ Calls Keystone URL and detects the available extensions """ + """Call Keystone URL and detects the available extensions.""" try: - httpclient = client.HTTPClient() if not url.endswith("/"): url += '/' - resp, body = httpclient.request("%sextensions" % url, "GET", - headers={'Accept': - 'application/json'}) + resp, body = self._request("%sextensions" % url, "GET", + headers={'Accept': + 'application/json'}) if resp.status_code in (200, 204): # some cases we get No Content - try: - results = {} - if 'extensions' in body: - if 'values' in body['extensions']: - # Parse correct format (per contract) - for extension in body['extensions']['values']: - alias, name = \ - self._get_extension_info( - extension['extension'] - ) - results[alias] = name - return results - else: - # Support incorrect, but prevalent format - for extension in body['extensions']: - alias, name = \ - self._get_extension_info(extension) - results[alias] = name - return results - else: - results['message'] = ("Unrecognized extensions " - "response from %s" % url) - return results - except KeyError: - raise exceptions.AuthorizationFailure() + if 'extensions' in body and 'values' in body['extensions']: + # Parse correct format (per contract) + extensions = body['extensions']['values'] + elif 'extensions' in body: + # Support incorrect, but prevalent format + extensions = body['extensions'] + else: + return dict(message=( + _('Unrecognized extensions response from %s') % url)) + + return dict(self._get_extension_info(e) for e in extensions) elif resp.status_code == 305: return self._check_keystone_extensions(resp['location']) else: - raise exceptions.from_response(resp, resp.text) - except Exception as e: - _logger.exception(e) + raise exceptions.from_response( + resp, "GET", "%sextensions" % url) + except Exception: + _logger.exception('Failed to check keystone extensions.') @staticmethod def _get_version_info(version, root_url): - """ Parses version information + """Parse version information. :param version: a dict of a Keystone version response :param root_url: string url used to construct @@ -199,7 +198,7 @@ def _get_version_info(version, root_url): @staticmethod def _get_extension_info(extension): - """ Parses extension information + """Parse extension information. :param extension: a dict of a Keystone extension response :returns: tuple - (alias, name) diff --git a/keystoneclient/generic/shell.py b/keystoneclient/generic/shell.py deleted file mode 100644 index 90fa4fd69..000000000 --- a/keystoneclient/generic/shell.py +++ /dev/null @@ -1,61 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from keystoneclient import utils -from keystoneclient.generic import client - -CLIENT_CLASS = client.Client - - -@utils.unauthenticated -def do_discover(cs, args): - """ - Discover Keystone servers and show authentication protocols and - extensions supported. - - Usage:: - - $ keystone discover - Keystone found at http://localhost:35357 - - - supports version v1.0 (DEPRECATED) here http://localhost:35357/v1.0 - - supports version v1.1 (CURRENT) here http://localhost:35357/v1.1 - - supports version v2.0 (CURRENT) here http://localhost:35357/v2.0 - - and RAX-KSKEY: Rackspace API Key Authentication Admin Extension - - and RAX-KSGRP: Rackspace Keystone Group Extensions - """ - if cs.endpoint: - versions = cs.discover(cs.endpoint) - elif cs.auth_url: - versions = cs.discover(cs.auth_url) - else: - versions = cs.discover() - if versions: - if 'message' in versions: - print versions['message'] - for key, version in versions.iteritems(): - if key != 'message': - print (" - supports version %s (%s) here %s" % - (version['id'], version['status'], version['url'])) - extensions = cs.discover_extensions(version['url']) - if extensions: - for key, extension in extensions.iteritems(): - if key != 'message': - print (" - and %s: %s" % - (key, extension)) - else: - print "No Keystone-compatible endpoint found" diff --git a/keystoneclient/httpclient.py b/keystoneclient/httpclient.py new file mode 100644 index 000000000..8c38d1596 --- /dev/null +++ b/keystoneclient/httpclient.py @@ -0,0 +1,931 @@ +# Copyright 2010 Jacob Kaplan-Moss +# 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. +"""OpenStack Client interface. Handles the REST calls and responses.""" + +import importlib.metadata +import logging +import warnings + +from debtcollector import removals +from debtcollector import renames +from keystoneauth1 import adapter +from oslo_serialization import jsonutils +import packaging.version +import requests + +try: + import pickle # nosec(cjschaef): see bug 1534288 for details + + # NOTE(sdague): The conditional keyring import needs to only + # trigger if it's a version of keyring that's supported in global + # requirements. Update _min and _bad when that changes. + keyring_v = packaging.version.Version( + importlib.metadata.version('keyring') + ) + keyring_min = packaging.version.Version('5.5.1') + # This is a list of versions, e.g., pkg_resources.parse_version('3.3') + keyring_bad = [] + + if keyring_v >= keyring_min and keyring_v not in keyring_bad: + import keyring + else: + keyring = None +except (ImportError, importlib.metadata.PackageNotFoundError): + keyring = None + pickle = None + + +from keystoneclient import _discover +from keystoneclient import access +from keystoneclient.auth import base +from keystoneclient import baseclient +from keystoneclient import exceptions +from keystoneclient.i18n import _ +from keystoneclient import session as client_session + + +_logger = logging.getLogger(__name__) + +USER_AGENT = client_session.USER_AGENT +"""Default user agent string. + +This property is deprecated as of the 1.7.0 release in favor of +:data:`keystoneclient.session.USER_AGENT` and may be removed in the 2.0.0 +release. +""" + + +@removals.remove(message='Use keystoneclient.session.request instead.', + version='1.7.0', removal_version='2.0.0') +def request(*args, **kwargs): + """Make a request. + + This function is deprecated as of the 1.7.0 release in favor of + :func:`keystoneclient.session.request` and may be removed in the + 2.0.0 release. + """ + return client_session.request(*args, **kwargs) + + +class _FakeRequestSession(object): + """This object is a temporary hack that should be removed later. + + Keystoneclient has a cyclical dependency with its managers which is + preventing it from being cleaned up correctly. This is always bad but when + we switched to doing connection pooling this object wasn't getting cleaned + either and so we had left over TCP connections hanging around. + + Until we can fix the client cleanup we rollback the use of a requests + session and do individual connections like we used to. + """ + + def request(self, *args, **kwargs): + return requests.request(*args, **kwargs) + + +class _KeystoneAdapter(adapter.LegacyJsonAdapter): + """A wrapper layer to interface keystoneclient with a session. + + An adapter provides a generic interface between a client and the session to + provide client specific defaults. This object is passed to the managers. + Keystoneclient managers have some additional requirements of variables that + they expect to be present on the passed object. + + Subclass the existing adapter to provide those values that keystoneclient + managers expect. + """ + + @property + def user_id(self): + """Best effort to retrieve the user_id from the plugin. + + Some managers rely on being able to get the currently authenticated + user id. This is a problem when we are trying to abstract away the + details of an auth plugin. + + For example changing a user's password can require access to the + currently authenticated user_id. + + Perform a best attempt to fetch this data. It will work in the legacy + case and with identity plugins and be None otherwise which is the same + as the historical behavior. + """ + # the identity plugin case + try: + return self.session.auth.get_access(self.session).user_id + except AttributeError: # nosec(cjschaef): attempt legacy retrival, or + # return None + pass + + # there is a case that we explicitly allow (tested by our unit tests) + # that says you should be able to set the user_id on a legacy client + # and it should overwrite the one retrieved via authentication. If it's + # a legacy then self.session.auth is a client and we retrieve user_id. + try: + return self.session.auth.user_id + except AttributeError: # nosec(cjschaef): retrivals failed, return + # None + pass + + return None + + +class HTTPClient(baseclient.Client, base.BaseAuthPlugin): + """HTTP client. + + .. warning:: + + Creating an instance of this class without using the session argument + is deprecated as of the 1.7.0 release and may be removed in the 2.0.0 + release. + + :param string user_id: User ID for authentication. (optional) + :param string username: Username for authentication. (optional) + :param string user_domain_id: User's domain ID for authentication. + (optional) + :param string user_domain_name: User's domain name for authentication. + (optional) + :param string password: Password for authentication. (optional) + :param string domain_id: Domain ID for domain scoping. (optional) + :param string domain_name: Domain name for domain scoping. (optional) + :param string project_id: Project ID for project scoping. (optional) + :param string project_name: Project name for project scoping. (optional) + :param string project_domain_id: Project's domain ID for project scoping. + (optional) + :param string project_domain_name: Project's domain name for project + scoping. (optional) + :param string auth_url: Identity service endpoint for authorization. + :param string region_name: Name of a region to select when choosing an + endpoint from the service catalog. + :param integer timeout: This argument is deprecated as of the 1.7.0 release + in favor of session and may be removed in the 2.0.0 + release. (optional) + :param string endpoint: A user-supplied endpoint URL for the identity + service. Lazy-authentication is possible for API + service calls if endpoint is set at instantiation. + (optional) + :param string token: Token for authentication. (optional) + :param string cacert: This argument is deprecated as of the 1.7.0 release + in favor of session and may be removed in the 2.0.0 + release. (optional) + :param string key: This argument is deprecated as of the 1.7.0 release + in favor of session and may be removed in the 2.0.0 + release. (optional) + :param string cert: This argument is deprecated as of the 1.7.0 release + in favor of session and may be removed in the 2.0.0 + release. (optional) + :param boolean insecure: This argument is deprecated as of the 1.7.0 + release in favor of session and may be removed in + the 2.0.0 release. (optional) + :param string original_ip: This argument is deprecated as of the 1.7.0 + release in favor of session and may be removed + in the 2.0.0 release. (optional) + :param dict auth_ref: To allow for consumers of the client to manage their + own caching strategy, you may initialize a client + with a previously captured auth_reference (token). If + there are keyword arguments passed that also exist in + auth_ref, the value from the argument will take + precedence. + :param boolean use_keyring: Enables caching auth_ref into keyring. + default: False (optional) + :param boolean force_new_token: Keyring related parameter, forces request + for new token. default: False (optional) + :param integer stale_duration: Gap in seconds to determine if token from + keyring is about to expire. default: 30 + (optional) + :param string tenant_name: Tenant name. (optional) The tenant_name keyword + argument is deprecated as of the 1.7.0 release + in favor of project_name and may be removed in + the 2.0.0 release. + :param string tenant_id: Tenant id. (optional) The tenant_id keyword + argument is deprecated as of the 1.7.0 release in + favor of project_id and may be removed in the + 2.0.0 release. + :param string trust_id: Trust ID for trust scoping. (optional) + :param object session: A Session object to be used for + communicating with the identity service. + :type session: keystoneclient.session.Session + :param string service_name: The default service_name for URL discovery. + default: None (optional) + :param string interface: The default interface for URL discovery. + default: admin (v2), public (v3). (optional) + :param string endpoint_override: Always use this endpoint URL for requests + for this client. (optional) + :param auth: An auth plugin to use instead of the session one. (optional) + :type auth: keystoneclient.auth.base.BaseAuthPlugin + :param string user_agent: The User-Agent string to set. + default: python-keystoneclient (optional) + :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. (optional) + """ + + version = None + + @renames.renamed_kwarg('tenant_name', 'project_name', version='1.7.0', + removal_version='2.0.0') + @renames.renamed_kwarg('tenant_id', 'project_id', version='1.7.0', + removal_version='2.0.0') + def __init__(self, username=None, tenant_id=None, tenant_name=None, + password=None, auth_url=None, region_name=None, endpoint=None, + token=None, auth_ref=None, use_keyring=False, + force_new_token=False, stale_duration=None, user_id=None, + user_domain_id=None, user_domain_name=None, domain_id=None, + domain_name=None, project_id=None, project_name=None, + project_domain_id=None, project_domain_name=None, + trust_id=None, session=None, service_name=None, + interface='default', endpoint_override=None, auth=None, + user_agent=USER_AGENT, connect_retries=None, **kwargs): + # set baseline defaults + self.user_id = None + self.username = None + self.user_domain_id = None + self.user_domain_name = None + + self.domain_id = None + self.domain_name = None + + self.project_id = None + self.project_name = None + self.project_domain_id = None + self.project_domain_name = None + + self.auth_url = None + self._endpoint = None + self._management_url = None + + self.trust_id = None + + # if loading from a dictionary passed in via auth_ref, + # load values from AccessInfo parsing that dictionary + if auth_ref: + self.auth_ref = access.AccessInfo.factory(**auth_ref) + self.version = self.auth_ref.version + self.user_id = self.auth_ref.user_id + self.username = self.auth_ref.username + self.user_domain_id = self.auth_ref.user_domain_id + self.domain_id = self.auth_ref.domain_id + self.domain_name = self.auth_ref.domain_name + self.project_id = self.auth_ref.project_id + self.project_name = self.auth_ref.project_name + self.project_domain_id = self.auth_ref.project_domain_id + auth_urls = self.auth_ref.service_catalog.get_urls( + service_type='identity', endpoint_type='public', + region_name=region_name) + self.auth_url = auth_urls[0] + management_urls = self.auth_ref.service_catalog.get_urls( + service_type='identity', endpoint_type='admin', + region_name=region_name) + self._management_url = management_urls[0] + self.auth_token_from_user = self.auth_ref.auth_token + self.trust_id = self.auth_ref.trust_id + + # TODO(blk-u): Using self.auth_ref.service_catalog._region_name is + # deprecated and this code must be removed when the property is + # actually removed. + if self.auth_ref.has_service_catalog() and not region_name: + region_name = self.auth_ref.service_catalog._region_name + + else: + self.auth_ref = None + + # allow override of the auth_ref defaults from explicit + # values provided to the client + + # apply deprecated variables first, so modern variables override them + if tenant_id: + self.project_id = tenant_id + if tenant_name: + self.project_name = tenant_name + + # user-related attributes + self.password = password + if user_id: + self.user_id = user_id + if username: + self.username = username + if user_domain_id: + self.user_domain_id = user_domain_id + elif not (user_id or user_domain_name): + self.user_domain_id = 'default' + if user_domain_name: + self.user_domain_name = user_domain_name + + # domain-related attributes + if domain_id: + self.domain_id = domain_id + if domain_name: + self.domain_name = domain_name + + # project-related attributes + if project_id: + self.project_id = project_id + if project_name: + self.project_name = project_name + if project_domain_id: + self.project_domain_id = project_domain_id + elif not (project_id or project_domain_name): + self.project_domain_id = 'default' + if project_domain_name: + self.project_domain_name = project_domain_name + + # trust-related attributes + if trust_id: + self.trust_id = trust_id + + # endpoint selection + if auth_url: + self.auth_url = auth_url.rstrip('/') + if token: + self.auth_token_from_user = token + else: + self.auth_token_from_user = None + if endpoint: + self._endpoint = endpoint.rstrip('/') + self._auth_token = None + + if not session: + + warnings.warn( + 'Constructing an HTTPClient instance without using a session ' + 'is deprecated as of the 1.7.0 release and may be removed in ' + 'the 2.0.0 release.', DeprecationWarning) + + kwargs['session'] = _FakeRequestSession() + session = client_session.Session._construct(kwargs) + session.auth = self + + self.session = session + self.domain = '' + + version = ( + _discover.normalize_version_number(self.version) if self.version + else None) + + # NOTE(frickler): If we know we have v3, use the public interface as + # default, otherwise keep the historic default of admin + if interface == 'default': + if version == (3, 0): + interface = 'public' + else: + interface = 'admin' + + # NOTE(jamielennox): unfortunately we can't just use **kwargs here as + # it would incompatibly limit the kwargs that can be passed to __init__ + # try and keep this list in sync with adapter.Adapter.__init__ + self._adapter = _KeystoneAdapter(session, + service_type='identity', + service_name=service_name, + interface=interface, + region_name=region_name, + endpoint_override=endpoint_override, + version=version, + auth=auth, + user_agent=user_agent, + connect_retries=connect_retries) + + # NOTE(dstanek): This allows me to not have to change keystoneauth or + # to write an adapter to the adapter here. Splitting thing into + # multiple project isn't always all sunshine and roses. + self._adapter.include_metadata = kwargs.pop('include_metadata', False) + + # keyring setup + if use_keyring and keyring is None: + _logger.warning('Failed to load keyring modules.') + self.use_keyring = use_keyring and keyring is not None + self.force_new_token = force_new_token + self.stale_duration = stale_duration or access.STALE_TOKEN_DURATION + self.stale_duration = int(self.stale_duration) + + def get_token(self, session, **kwargs): + return self.auth_token + + @property + def auth_token(self): + if self._auth_token: + return self._auth_token + if self.auth_ref: + if self.auth_ref.will_expire_soon(self.stale_duration): + self.authenticate() + return self.auth_ref.auth_token + if self.auth_token_from_user: + return self.auth_token_from_user + + def get_endpoint(self, session, interface=None, **kwargs): + if interface == 'public' or interface is base.AUTH_INTERFACE: + return self.auth_url + else: + return self.management_url + + def get_user_id(self, session, **kwargs): + return self.auth_ref.user_id + + def get_project_id(self, session, **kwargs): + return self.auth_ref.project_id + + @auth_token.setter + def auth_token(self, value): + """Override the auth_token. + + If an application sets auth_token explicitly then it will always be + used and override any past or future retrieved token. + """ + self._auth_token = value + + @auth_token.deleter + def auth_token(self): + self._auth_token = None + self.auth_token_from_user = None + + @property + def service_catalog(self): + """Return this client's service catalog.""" + try: + return self.auth_ref.service_catalog + except AttributeError: + return None + + def has_service_catalog(self): + """Return True if this client provides a service catalog.""" + return self.auth_ref and self.auth_ref.has_service_catalog() + + @property + def tenant_id(self): + """Provide read-only backwards compatibility for tenant_id. + + .. warning:: + + This is deprecated as of the 1.7.0 release in favor of project_id + and may be removed in the 2.0.0 release. + """ + warnings.warn( + 'tenant_id is deprecated as of the 1.7.0 release in favor of ' + 'project_id and may be removed in the 2.0.0 release.', + DeprecationWarning) + + return self.project_id + + @property + def tenant_name(self): + """Provide read-only backwards compatibility for tenant_name. + + .. warning:: + + This is deprecated as of the 1.7.0 release in favor of project_name + and may be removed in the 2.0.0 release. + """ + warnings.warn( + 'tenant_name is deprecated as of the 1.7.0 release in favor of ' + 'project_name and may be removed in the 2.0.0 release.', + DeprecationWarning) + + return self.project_name + + def authenticate(self, username=None, password=None, tenant_name=None, + tenant_id=None, auth_url=None, token=None, + user_id=None, domain_name=None, domain_id=None, + project_name=None, project_id=None, user_domain_id=None, + user_domain_name=None, project_domain_id=None, + project_domain_name=None, trust_id=None, + region_name=None): + """Authenticate user. + + Uses the data provided at instantiation to authenticate against + the Identity server. This may use either a username and password + or token for authentication. If a tenant name or id was provided + then the resulting authenticated client will be scoped to that + tenant and contain a service catalog of available endpoints. + + With the v2.0 API, if a tenant name or ID is not provided, the + authentication token returned will be 'unscoped' and limited in + capabilities until a fully-scoped token is acquired. + + With the v3 API, if a domain name or id was provided then the resulting + authenticated client will be scoped to that domain. If a project name + or ID is not provided, and the authenticating user has a default + project configured, the authentication token returned will be 'scoped' + to the default project. Otherwise, the authentication token returned + will be 'unscoped' and limited in capabilities until a fully-scoped + token is acquired. + + With the v3 API, with the OS-TRUST extension enabled, the trust_id can + be provided to allow project-specific role delegation between users + + If successful, sets the self.auth_ref and self.auth_token with + the returned token. If not already set, will also set + self.management_url from the details provided in the token. + + :returns: ``True`` if authentication was successful. + :raises keystoneclient.exceptions.AuthorizationFailure: if unable to + authenticate or validate the existing authorization token + :raises keystoneclient.exceptions.ValueError: if insufficient + parameters are used. + + If keyring is used, token is retrieved from keyring instead. + Authentication will only be necessary if any of the following + conditions are met: + + * keyring is not used + * if token is not found in keyring + * if token retrieved from keyring is expired or about to + expired (as determined by stale_duration) + * if force_new_token is true + + """ + auth_url = auth_url or self.auth_url + user_id = user_id or self.user_id + username = username or self.username + password = password or self.password + + user_domain_id = user_domain_id or self.user_domain_id + user_domain_name = user_domain_name or self.user_domain_name + domain_id = domain_id or self.domain_id + domain_name = domain_name or self.domain_name + project_id = project_id or tenant_id or self.project_id + project_name = project_name or tenant_name or self.project_name + project_domain_id = project_domain_id or self.project_domain_id + project_domain_name = project_domain_name or self.project_domain_name + + trust_id = trust_id or self.trust_id + region_name = region_name or self._adapter.region_name + + if not token: + token = self.auth_token_from_user + if (not token and self.auth_ref and not + self.auth_ref.will_expire_soon(self.stale_duration)): + token = self.auth_ref.auth_token + + kwargs = { + 'auth_url': auth_url, + 'user_id': user_id, + 'username': username, + 'user_domain_id': user_domain_id, + 'user_domain_name': user_domain_name, + 'domain_id': domain_id, + 'domain_name': domain_name, + 'project_id': project_id, + 'project_name': project_name, + 'project_domain_id': project_domain_id, + 'project_domain_name': project_domain_name, + 'token': token, + 'trust_id': trust_id, + } + (keyring_key, auth_ref) = self.get_auth_ref_from_keyring(**kwargs) + new_token_needed = False + if auth_ref is None or self.force_new_token: + new_token_needed = True + kwargs['password'] = password + resp = self.get_raw_token_from_identity_service(**kwargs) + + if isinstance(resp, access.AccessInfo): + self.auth_ref = resp + else: + self.auth_ref = access.AccessInfo.factory(*resp) + + # NOTE(jamielennox): The original client relies on being able to + # push the region name into the service catalog but new auth + # it in. + if region_name: + self.auth_ref.service_catalog._region_name = region_name + else: + self.auth_ref = auth_ref + + self.process_token(region_name=region_name) + if new_token_needed: + self.store_auth_ref_into_keyring(keyring_key) + return True + + def _build_keyring_key(self, **kwargs): + """Create a unique key for keyring. + + Used to store and retrieve auth_ref from keyring. + + Return a slash-separated string of values ordered by key name. + + """ + return '/'.join([kwargs[k] or '?' for k in sorted(kwargs)]) + + def get_auth_ref_from_keyring(self, **kwargs): + """Retrieve auth_ref from keyring. + + If auth_ref is found in keyring, (keyring_key, auth_ref) is returned. + Otherwise, (keyring_key, None) is returned. + + :returns: (keyring_key, auth_ref) or (keyring_key, None) + :returns: or (None, None) if use_keyring is not set in the object + + """ + keyring_key = None + auth_ref = None + if self.use_keyring: + keyring_key = self._build_keyring_key(**kwargs) + try: + auth_ref = keyring.get_password("keystoneclient_auth", + keyring_key) + if auth_ref: + auth_ref = pickle.loads(auth_ref) # nosec(cjschaef): see + # bug 1534288 + if auth_ref.will_expire_soon(self.stale_duration): + # token has expired, don't use it + auth_ref = None + except Exception as e: + auth_ref = None + _logger.warning('Unable to retrieve token from keyring %s', e) + return (keyring_key, auth_ref) + + def store_auth_ref_into_keyring(self, keyring_key): + """Store auth_ref into keyring.""" + if self.use_keyring: + try: + keyring.set_password("keystoneclient_auth", + keyring_key, + pickle.dumps(self.auth_ref)) # nosec + # (cjschaef): see bug 1534288 + except Exception as e: + _logger.warning("Failed to store token into keyring %s", e) + + def _process_management_url(self, region_name): + try: + self._management_url = self.auth_ref.service_catalog.url_for( + service_type='identity', + endpoint_type='admin', + region_name=region_name) + except exceptions.EndpointNotFound as e: + _logger.debug("Failed to find endpoint for management url %s", e) + + def process_token(self, region_name=None): + """Extract and process information from the new auth_ref. + + And set the relevant authentication information. + """ + # if we got a response without a service catalog, set the local + # list of tenants for introspection, and leave to client user + # to determine what to do. Otherwise, load up the service catalog + if self.auth_ref.project_scoped: + if not self.auth_ref.tenant_id: + raise exceptions.AuthorizationFailure( + _("Token didn't provide tenant_id")) + self._process_management_url(region_name) + self.project_name = self.auth_ref.tenant_name + self.project_id = self.auth_ref.tenant_id + + if not self.auth_ref.user_id: + raise exceptions.AuthorizationFailure( + _("Token didn't provide user_id")) + + self.user_id = self.auth_ref.user_id + + self.auth_domain_id = self.auth_ref.domain_id + self.auth_tenant_id = self.auth_ref.tenant_id + self.auth_user_id = self.auth_ref.user_id + + @property + def management_url(self): + return self._endpoint or self._management_url + + @management_url.setter + def management_url(self, value): + # NOTE(jamielennox): it's debatable here whether we should set + # _endpoint or _management_url. As historically management_url was set + # permanently setting _endpoint would better match that behaviour. + self._endpoint = value + + def get_raw_token_from_identity_service(self, auth_url, username=None, + password=None, tenant_name=None, + tenant_id=None, token=None, + user_id=None, user_domain_id=None, + user_domain_name=None, + domain_id=None, domain_name=None, + project_id=None, project_name=None, + project_domain_id=None, + project_domain_name=None, + trust_id=None): + """Authenticate against the Identity API and get a token. + + Not implemented here because auth protocols should be API + version-specific. + + Expected to authenticate or validate an existing authentication + reference already associated with the client. Invoking this call + *always* makes a call to the Identity service. + + :returns: (``resp``, ``body``) + + """ + raise NotImplementedError + + def serialize(self, entity): + return jsonutils.dumps(entity) + + @removals.remove(version='1.7.0', removal_version='2.0.0') + def request(self, *args, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around requests.request to handle tasks such as + setting headers, JSON encoding/decoding, and error handling. + + .. warning:: + + *DEPRECATED*: This function is no longer used. It was designed to + be used only by the managers and the managers now receive an + adapter so this function is no longer on the standard request path. + This may be removed in the 2.0.0 release. + """ + return self._request(*args, **kwargs) + + def _request(self, *args, **kwargs): + kwargs.setdefault('authenticated', False) + return self._adapter.request(*args, **kwargs) + + def _cs_request(self, url, method, management=True, **kwargs): + """Make an authenticated request to keystone endpoint. + + Request are made to keystone endpoint by concatenating + self.management_url and url and passing in method and + any associated kwargs. + """ + if not management: + endpoint_filter = kwargs.setdefault('endpoint_filter', {}) + endpoint_filter.setdefault('interface', 'public') + + kwargs.setdefault('authenticated', None) + return self._request(url, method, **kwargs) + + @removals.remove(version='1.7.0', removal_version='2.0.0') + def get(self, url, **kwargs): + """Perform an authenticated GET request. + + This calls :py:meth:`.request()` with ``method`` set to ``GET`` and an + authentication token if one is available. + + .. warning:: + + *DEPRECATED*: This function is no longer used and is deprecated as + of the 1.7.0 release and may be removed in the 2.0.0 release. It + was designed to be used by the managers and the managers now + receive an adapter so this function is no longer on the standard + request path. + """ + return self._cs_request(url, 'GET', **kwargs) + + @removals.remove(version='1.7.0', removal_version='2.0.0') + def head(self, url, **kwargs): + """Perform an authenticated HEAD request. + + This calls :py:meth:`.request()` with ``method`` set to ``HEAD`` and an + authentication token if one is available. + + .. warning:: + + *DEPRECATED*: This function is no longer used and is deprecated as + of the 1.7.0 release and may be removed in the 2.0.0 release. It + was designed to be used by the managers and the managers now + receive an adapter so this function is no longer on the standard + request path. + """ + return self._cs_request(url, 'HEAD', **kwargs) + + @removals.remove(version='1.7.0', removal_version='2.0.0') + def post(self, url, **kwargs): + """Perform an authenticate POST request. + + This calls :py:meth:`.request()` with ``method`` set to ``POST`` and an + authentication token if one is available. + + .. warning:: + + *DEPRECATED*: This function is no longer used and is deprecated as + of the 1.7.0 release and may be removed in the 2.0.0 release. It + was designed to be used by the managers and the managers now + receive an adapter so this function is no longer on the standard + request path. + """ + return self._cs_request(url, 'POST', **kwargs) + + @removals.remove(version='1.7.0', removal_version='2.0.0') + def put(self, url, **kwargs): + """Perform an authenticate PUT request. + + This calls :py:meth:`.request()` with ``method`` set to ``PUT`` and an + authentication token if one is available. + + .. warning:: + + *DEPRECATED*: This function is no longer used and is deprecated as + of the 1.7.0 release and may be removed in the 2.0.0 release. It + was designed to be used by the managers and the managers now + receive an adapter so this function is no longer on the standard + request path. + """ + return self._cs_request(url, 'PUT', **kwargs) + + @removals.remove(version='1.7.0', removal_version='2.0.0') + def patch(self, url, **kwargs): + """Perform an authenticate PATCH request. + + This calls :py:meth:`.request()` with ``method`` set to ``PATCH`` and + an authentication token if one is available. + + .. warning:: + + *DEPRECATED*: This function is no longer used and is deprecated as + of the 1.7.0 release and may be removed in the 2.0.0 release. It + was designed to be used by the managers and the managers now + receive an adapter so this function is no longer on the standard + request path. + """ + return self._cs_request(url, 'PATCH', **kwargs) + + @removals.remove(version='1.7.0', removal_version='2.0.0') + def delete(self, url, **kwargs): + """Perform an authenticate DELETE request. + + This calls :py:meth:`.request()` with ``method`` set to ``DELETE`` and + an authentication token if one is available. + + .. warning:: + + *DEPRECATED*: This function is no longer used and is deprecated as + of the 1.7.0 release and may be removed in the 2.0.0 release. It + was designed to be used by the managers and the managers now + receive an adapter so this function is no longer on the standard + request path. + """ + return self._cs_request(url, 'DELETE', **kwargs) + + deprecated_session_variables = {'original_ip': None, + 'cert': None, + 'timeout': None, + 'verify_cert': 'verify'} + + deprecated_adapter_variables = {'region_name': None} + + def __getattr__(self, name): + """Fetch deprecated session variables.""" + try: + var_name = self.deprecated_session_variables[name] + except KeyError: # nosec(cjschaef): try adapter variable or raise + # an AttributeError + pass + else: + warnings.warn( + 'The %s session variable is deprecated as of the 1.7.0 ' + 'release and may be removed in the 2.0.0 release' % name, + DeprecationWarning) + return getattr(self.session, var_name or name) + + try: + var_name = self.deprecated_adapter_variables[name] + except KeyError: # nosec(cjschaef): raise an AttributeError + pass + else: + warnings.warn( + 'The %s adapter variable is deprecated as of the 1.7.0 ' + 'release and may be removed in the 2.0.0 release' % name, + DeprecationWarning) + return getattr(self._adapter, var_name or name) + + raise AttributeError(_("Unknown Attribute: %s") % name) + + def __setattr__(self, name, val): + """Assign value to deprecated seesion variables.""" + try: + var_name = self.deprecated_session_variables[name] + except KeyError: # nosec(cjschaef): try adapter variable or call + # parent class's __setattr__ + pass + else: + warnings.warn( + 'The %s session variable is deprecated as of the 1.7.0 ' + 'release and may be removed in the 2.0.0 release' % name, + DeprecationWarning) + return setattr(self.session, var_name or name) + + try: + var_name = self.deprecated_adapter_variables[name] + except KeyError: # nosec(cjschaef): call parent class's __setattr__ + pass + else: + warnings.warn( + 'The %s adapter variable is deprecated as of the 1.7.0 ' + 'release and may be removed in the 2.0.0 release' % name, + DeprecationWarning) + return setattr(self._adapter, var_name or name) + + super(HTTPClient, self).__setattr__(name, val) diff --git a/keystoneclient/i18n.py b/keystoneclient/i18n.py new file mode 100644 index 000000000..f3726d199 --- /dev/null +++ b/keystoneclient/i18n.py @@ -0,0 +1,27 @@ +# Copyright 2014 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""oslo.i18n integration module. + +See https://docs.openstack.org/oslo.i18n/latest/user/index.html . + +""" + +import oslo_i18n + + +_translators = oslo_i18n.TranslatorFactory(domain='keystoneclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary diff --git a/keystoneclient/locale/keystoneclient.pot b/keystoneclient/locale/keystoneclient.pot deleted file mode 100644 index a96686484..000000000 --- a/keystoneclient/locale/keystoneclient.pot +++ /dev/null @@ -1,20 +0,0 @@ -# Translations template for python-keystoneclient. -# Copyright (C) 2012 ORGANIZATION -# This file is distributed under the same license as the -# python-keystoneclient project. -# FIRST AUTHOR , 2012. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: python-keystoneclient 0.1.3.12\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2012-09-29 16:02-0700\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" - diff --git a/keystoneclient/middleware/auth_token.py b/keystoneclient/middleware/auth_token.py deleted file mode 100644 index 7d332205e..000000000 --- a/keystoneclient/middleware/auth_token.py +++ /dev/null @@ -1,988 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2012 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -TOKEN-BASED AUTH MIDDLEWARE - -This WSGI component: - -* Verifies that incoming client requests have valid tokens by validating - tokens with the auth service. -* Rejects unauthenticated requests UNLESS it is in 'delay_auth_decision' - mode, which means the final decision is delegated to the downstream WSGI - component (usually the OpenStack service) -* Collects and forwards identity information based on a valid token - such as user name, tenant, etc - -Refer to: http://keystone.openstack.org/middlewarearchitecture.html - -HEADERS -------- - -* Headers starting with HTTP\_ is a standard http header -* Headers starting with HTTP_X is an extended http header - -Coming in from initial call from client or customer -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -HTTP_X_AUTH_TOKEN - The client token being passed in. - -HTTP_X_STORAGE_TOKEN - The client token being passed in (legacy Rackspace use) to support - swift/cloud files - -Used for communication between components -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -WWW-Authenticate - HTTP header returned to a user indicating which endpoint to use - to retrieve a new token - -What we add to the request for use by the OpenStack service -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -HTTP_X_IDENTITY_STATUS - 'Confirmed' or 'Invalid' - The underlying service will only see a value of 'Invalid' if the Middleware - is configured to run in 'delay_auth_decision' mode - -HTTP_X_TENANT_ID - Identity service managed unique identifier, string - -HTTP_X_TENANT_NAME - Unique tenant identifier, string - -HTTP_X_USER_ID - Identity-service managed unique identifier, string - -HTTP_X_USER_NAME - Unique user identifier, string - -HTTP_X_ROLES - Comma delimited list of case-sensitive Roles - -HTTP_X_SERVICE_CATALOG - json encoded keystone service catalog (optional). - -HTTP_X_TENANT - *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME - Keystone-assigned unique identifier, deprecated - -HTTP_X_USER - *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME - Unique user name, string - -HTTP_X_ROLE - *Deprecated* in favor of HTTP_X_ROLES - This is being renamed, and the new header contains the same data. - -OTHER ENVIRONMENT VARIABLES ---------------------------- - -keystone.token_info - Information about the token discovered in the process of - validation. This may include extended information returned by the - Keystone token validation call, as well as basic information about - the tenant and user. - -""" - -import datetime -import httplib -import json -import logging -import os -import stat -import time -import urllib -import webob.exc - -from keystoneclient.openstack.common import jsonutils -from keystoneclient.common import cms -from keystoneclient import utils -from keystoneclient.middleware import memcache_crypt -from keystoneclient.openstack.common import timeutils - -CONF = None -try: - from openstack.common import cfg - CONF = cfg.CONF -except ImportError: - # cfg is not a library yet, try application copies - for app in 'nova', 'glance', 'quantum', 'cinder': - try: - cfg = __import__('%s.openstack.common.cfg' % app, - fromlist=['%s.openstack.common' % app]) - # test which application middleware is running in - if hasattr(cfg, 'CONF') and 'config_file' in cfg.CONF: - CONF = cfg.CONF - break - except ImportError: - pass -if not CONF: - from keystoneclient.openstack.common import cfg - CONF = cfg.CONF - -# alternative middleware configuration in the main application's -# configuration file e.g. in nova.conf -# [keystone_authtoken] -# auth_host = 127.0.0.1 -# auth_port = 35357 -# auth_protocol = http -# admin_tenant_name = admin -# admin_user = admin -# admin_password = badpassword - -# when deploy Keystone auth_token middleware with Swift, user may elect -# to use Swift memcache instead of the local Keystone memcache. Swift memcache -# is passed in from the request environment and its identified by the -# 'swift.cache' key. However it could be different, depending on deployment. -# To use Swift memcache, you must set the 'cache' option to the environment -# key where the Swift cache object is stored. -opts = [ - cfg.StrOpt('auth_admin_prefix', default=''), - cfg.StrOpt('auth_host', default='127.0.0.1'), - cfg.IntOpt('auth_port', default=35357), - cfg.StrOpt('auth_protocol', default='https'), - cfg.StrOpt('auth_uri', default=None), - cfg.BoolOpt('delay_auth_decision', default=False), - cfg.StrOpt('admin_token'), - cfg.StrOpt('admin_user'), - cfg.StrOpt('admin_password'), - cfg.StrOpt('admin_tenant_name', default='admin'), - cfg.StrOpt('cache', default=None), # env key for the swift cache - cfg.StrOpt('certfile'), - cfg.StrOpt('keyfile'), - cfg.StrOpt('signing_dir', - default=os.path.expanduser('~/keystone-signing')), - cfg.ListOpt('memcache_servers'), - cfg.IntOpt('token_cache_time', default=300), - cfg.StrOpt('memcache_security_strategy', default=None), - cfg.StrOpt('memcache_secret_key', default=None), -] -CONF.register_opts(opts, group='keystone_authtoken') - - -def will_expire_soon(expiry): - """ Determines if expiration is about to occur. - - :param expiry: a datetime of the expected expiration - :returns: boolean : true if expiration is within 30 seconds - """ - soon = (timeutils.utcnow() + datetime.timedelta(seconds=30)) - return expiry < soon - - -def safe_quote(s): - """URL-encode strings that are not already URL-encoded.""" - return urllib.quote(s) if s == urllib.unquote(s) else s - - -class InvalidUserToken(Exception): - pass - - -class ServiceError(Exception): - pass - - -class ConfigurationError(Exception): - pass - - -class AuthProtocol(object): - """Auth Middleware that handles authenticating client calls.""" - - def __init__(self, app, conf): - self.LOG = logging.getLogger(conf.get('log_name', __name__)) - self.LOG.info('Starting keystone auth_token middleware') - self.conf = conf - self.app = app - - # delay_auth_decision means we still allow unauthenticated requests - # through and we let the downstream service make the final decision - self.delay_auth_decision = (self._conf_get('delay_auth_decision') in - (True, 'true', 't', '1', 'on', 'yes', 'y')) - - # where to find the auth service (we use this to validate tokens) - self.auth_host = self._conf_get('auth_host') - self.auth_port = int(self._conf_get('auth_port')) - self.auth_protocol = self._conf_get('auth_protocol') - if self.auth_protocol == 'http': - self.http_client_class = httplib.HTTPConnection - else: - self.http_client_class = httplib.HTTPSConnection - - self.auth_admin_prefix = self._conf_get('auth_admin_prefix') - self.auth_uri = self._conf_get('auth_uri') - if self.auth_uri is None: - self.auth_uri = '%s://%s:%s' % (self.auth_protocol, - self.auth_host, - self.auth_port) - - # SSL - self.cert_file = self._conf_get('certfile') - self.key_file = self._conf_get('keyfile') - - #signing - self.signing_dirname = self._conf_get('signing_dir') - self.LOG.info('Using %s as cache directory for signing certificate' % - self.signing_dirname) - if (os.path.exists(self.signing_dirname) and - not os.access(self.signing_dirname, os.W_OK)): - raise ConfigurationError("unable to access signing dir %s" % - self.signing_dirname) - - if not os.path.exists(self.signing_dirname): - os.makedirs(self.signing_dirname) - #will throw IOError if it cannot change permissions - os.chmod(self.signing_dirname, stat.S_IRWXU) - - val = '%s/signing_cert.pem' % self.signing_dirname - self.signing_cert_file_name = val - val = '%s/cacert.pem' % self.signing_dirname - self.ca_file_name = val - val = '%s/revoked.pem' % self.signing_dirname - self.revoked_file_name = val - - # Credentials used to verify this component with the Auth service since - # validating tokens is a privileged call - self.admin_token = self._conf_get('admin_token') - self.admin_token_expiry = None - self.admin_user = self._conf_get('admin_user') - self.admin_password = self._conf_get('admin_password') - self.admin_tenant_name = self._conf_get('admin_tenant_name') - - # Token caching via memcache - self._cache = None - self._use_keystone_cache = False - self._cache_initialized = False # cache already initialzied? - # memcache value treatment, ENCRYPT or MAC - self._memcache_security_strategy = \ - self._conf_get('memcache_security_strategy') - if self._memcache_security_strategy is not None: - self._memcache_security_strategy = \ - self._memcache_security_strategy.upper() - self._memcache_secret_key = \ - self._conf_get('memcache_secret_key') - self._assert_valid_memcache_protection_config() - # By default the token will be cached for 5 minutes - self.token_cache_time = int(self._conf_get('token_cache_time')) - self._token_revocation_list = None - self._token_revocation_list_fetched_time = None - cache_timeout = datetime.timedelta(seconds=0) - self.token_revocation_list_cache_timeout = cache_timeout - - def _assert_valid_memcache_protection_config(self): - if self._memcache_security_strategy: - if self._memcache_security_strategy not in ('MAC', 'ENCRYPT'): - raise Exception('memcache_security_strategy must be ' - 'ENCRYPT or MAC') - if not self._memcache_secret_key: - raise Exception('mecmache_secret_key must be defined when ' - 'a memcache_security_strategy is defined') - - def _init_cache(self, env): - cache = self._conf_get('cache') - memcache_servers = self._conf_get('memcache_servers') - if cache and env.get(cache, None) is not None: - # use the cache from the upstream filter - self.LOG.info('Using %s memcache for caching token', cache) - self._cache = env.get(cache) - else: - # use Keystone memcache - memcache_servers = self._conf_get('memcache_servers') - if memcache_servers: - try: - import memcache - self.LOG.info('Using Keystone memcache for caching token') - self._cache = memcache.Client(memcache_servers) - self._use_keystone_cache = True - except ImportError as e: - msg = 'disabled caching due to missing libraries %s' % (e) - self.LOG.warn(msg) - self._cache_initialized = True - - def _conf_get(self, name): - # try config from paste-deploy first - if name in self.conf: - return self.conf[name] - else: - return CONF.keystone_authtoken[name] - - def __call__(self, env, start_response): - """Handle incoming request. - - Authenticate send downstream on success. Reject request if - we can't authenticate. - - """ - self.LOG.debug('Authenticating user token') - - # initialize memcache if we haven't done so - if not self._cache_initialized: - self._init_cache(env) - - try: - self._remove_auth_headers(env) - user_token = self._get_user_token_from_header(env) - token_info = self._validate_user_token(user_token) - env['keystone.token_info'] = token_info - user_headers = self._build_user_headers(token_info) - self._add_headers(env, user_headers) - return self.app(env, start_response) - - except InvalidUserToken: - if self.delay_auth_decision: - self.LOG.info( - 'Invalid user token - deferring reject downstream') - self._add_headers(env, {'X-Identity-Status': 'Invalid'}) - return self.app(env, start_response) - else: - self.LOG.info('Invalid user token - rejecting request') - return self._reject_request(env, start_response) - - except ServiceError as e: - self.LOG.critical('Unable to obtain admin token: %s' % e) - resp = webob.exc.HTTPServiceUnavailable() - return resp(env, start_response) - - def _remove_auth_headers(self, env): - """Remove headers so a user can't fake authentication. - - :param env: wsgi request environment - - """ - auth_headers = ( - 'X-Identity-Status', - 'X-Tenant-Id', - 'X-Tenant-Name', - 'X-User-Id', - 'X-User-Name', - 'X-Roles', - 'X-Service-Catalog', - # Deprecated - 'X-User', - 'X-Tenant', - 'X-Role', - ) - self.LOG.debug('Removing headers from request environment: %s' % - ','.join(auth_headers)) - self._remove_headers(env, auth_headers) - - def _get_user_token_from_header(self, env): - """Get token id from request. - - :param env: wsgi request environment - :return token id - :raises InvalidUserToken if no token is provided in request - - """ - token = self._get_header(env, 'X-Auth-Token', - self._get_header(env, 'X-Storage-Token')) - if token: - return token - else: - if not self.delay_auth_decision: - self.LOG.warn("Unable to find authentication token" - " in headers") - self.LOG.debug("Headers: %s", env) - raise InvalidUserToken('Unable to find token in headers') - - def _reject_request(self, env, start_response): - """Redirect client to auth server. - - :param env: wsgi request environment - :param start_response: wsgi response callback - :returns HTTPUnauthorized http response - - """ - headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % self.auth_uri)] - resp = webob.exc.HTTPUnauthorized('Authentication required', headers) - return resp(env, start_response) - - def get_admin_token(self): - """Return admin token, possibly fetching a new one. - - if self.admin_token_expiry is set from fetching an admin token, check - it for expiration, and request a new token is the existing token - is about to expire. - - :return admin token id - :raise ServiceError when unable to retrieve token from keystone - - """ - if self.admin_token_expiry: - if will_expire_soon(self.admin_token_expiry): - self.admin_token = None - - if not self.admin_token: - (self.admin_token, - self.admin_token_expiry) = self._request_admin_token() - - return self.admin_token - - def _get_http_connection(self): - if self.auth_protocol == 'http': - return self.http_client_class(self.auth_host, self.auth_port) - else: - return self.http_client_class(self.auth_host, - self.auth_port, - self.key_file, - self.cert_file) - - def _http_request(self, method, path): - """HTTP request helper used to make unspecified content type requests. - - :param method: http method - :param path: relative request url - :return (http response object) - :raise ServerError when unable to communicate with keystone - - """ - conn = self._get_http_connection() - - try: - conn.request(method, path) - response = conn.getresponse() - body = response.read() - except Exception as e: - self.LOG.error('HTTP connection exception: %s' % e) - raise ServiceError('Unable to communicate with keystone') - finally: - conn.close() - - return response, body - - def _json_request(self, method, path, body=None, additional_headers=None): - """HTTP request helper used to make json requests. - - :param method: http method - :param path: relative request url - :param body: dict to encode to json as request body. Optional. - :param additional_headers: dict of additional headers to send with - http request. Optional. - :return (http response object, response body parsed as json) - :raise ServerError when unable to communicate with keystone - - """ - conn = self._get_http_connection() - - kwargs = { - 'headers': { - 'Content-type': 'application/json', - 'Accept': 'application/json', - }, - } - - if additional_headers: - kwargs['headers'].update(additional_headers) - - if body: - kwargs['body'] = jsonutils.dumps(body) - - full_path = self.auth_admin_prefix + path - try: - conn.request(method, full_path, **kwargs) - response = conn.getresponse() - body = response.read() - except Exception as e: - self.LOG.error('HTTP connection exception: %s' % e) - raise ServiceError('Unable to communicate with keystone') - finally: - conn.close() - - try: - data = jsonutils.loads(body) - except ValueError: - self.LOG.debug('Keystone did not return json-encoded body') - data = {} - - return response, data - - def _request_admin_token(self): - """Retrieve new token as admin user from keystone. - - :return token id upon success - :raises ServerError when unable to communicate with keystone - - """ - params = { - 'auth': { - 'passwordCredentials': { - 'username': self.admin_user, - 'password': self.admin_password, - }, - 'tenantName': self.admin_tenant_name, - } - } - - response, data = self._json_request('POST', - '/v2.0/tokens', - body=params) - - try: - token = data['access']['token']['id'] - expiry = data['access']['token']['expires'] - assert token - assert expiry - datetime_expiry = timeutils.parse_isotime(expiry) - return (token, timeutils.normalize_time(datetime_expiry)) - except (AssertionError, KeyError): - self.LOG.warn( - "Unexpected response from keystone service: %s", data) - raise ServiceError('invalid json response') - except (ValueError): - self.LOG.warn( - "Unable to parse expiration time from token: %s", data) - raise ServiceError('invalid json response') - - def _validate_user_token(self, user_token, retry=True): - """Authenticate user using PKI - - :param user_token: user's token id - :param retry: Ignored, as it is not longer relevant - :return uncrypted body of the token if the token is valid - :raise InvalidUserToken if token is rejected - :no longer raises ServiceError since it no longer makes RPC - - """ - try: - token_id = cms.cms_hash_token(user_token) - cached = self._cache_get(token_id) - if cached: - return cached - if cms.is_ans1_token(user_token): - verified = self.verify_signed_token(user_token) - data = json.loads(verified) - else: - data = self.verify_uuid_token(user_token, retry) - self._cache_put(token_id, data) - return data - except Exception as e: - self.LOG.debug('Token validation failure.', exc_info=True) - self._cache_store_invalid(user_token) - self.LOG.warn("Authorization failed for token %s", user_token) - raise InvalidUserToken('Token authorization failed') - - def _build_user_headers(self, token_info): - """Convert token object into headers. - - Build headers that represent authenticated user: - * X_IDENTITY_STATUS: Confirmed or Invalid - * X_TENANT_ID: id of tenant if tenant is present - * X_TENANT_NAME: name of tenant if tenant is present - * X_USER_ID: id of user - * X_USER_NAME: name of user - * X_ROLES: list of roles - * X_SERVICE_CATALOG: service catalog - - Additional (deprecated) headers include: - * X_USER: name of user - * X_TENANT: For legacy compatibility before we had ID and Name - * X_ROLE: list of roles - - :param token_info: token object returned by keystone on authentication - :raise InvalidUserToken when unable to parse token object - - """ - user = token_info['access']['user'] - token = token_info['access']['token'] - roles = ','.join([role['name'] for role in user.get('roles', [])]) - - def get_tenant_info(): - """Returns a (tenant_id, tenant_name) tuple from context.""" - def essex(): - """Essex puts the tenant ID and name on the token.""" - return (token['tenant']['id'], token['tenant']['name']) - - def pre_diablo(): - """Pre-diablo, Keystone only provided tenantId.""" - return (token['tenantId'], token['tenantId']) - - def default_tenant(): - """Assume the user's default tenant.""" - return (user['tenantId'], user['tenantName']) - - for method in [essex, pre_diablo, default_tenant]: - try: - return method() - except KeyError: - pass - - raise InvalidUserToken('Unable to determine tenancy.') - - tenant_id, tenant_name = get_tenant_info() - - user_id = user['id'] - user_name = user['name'] - - rval = { - 'X-Identity-Status': 'Confirmed', - 'X-Tenant-Id': tenant_id, - 'X-Tenant-Name': tenant_name, - 'X-User-Id': user_id, - 'X-User-Name': user_name, - 'X-Roles': roles, - # Deprecated - 'X-User': user_name, - 'X-Tenant': tenant_name, - 'X-Role': roles, - } - - try: - catalog = token_info['access']['serviceCatalog'] - rval['X-Service-Catalog'] = jsonutils.dumps(catalog) - except KeyError: - pass - - return rval - - def _header_to_env_var(self, key): - """Convert header to wsgi env variable. - - :param key: http header name (ex. 'X-Auth-Token') - :return wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN') - - """ - return 'HTTP_%s' % key.replace('-', '_').upper() - - def _add_headers(self, env, headers): - """Add http headers to environment.""" - for (k, v) in headers.iteritems(): - env_key = self._header_to_env_var(k) - env[env_key] = v - - def _remove_headers(self, env, keys): - """Remove http headers from environment.""" - for k in keys: - env_key = self._header_to_env_var(k) - try: - del env[env_key] - except KeyError: - pass - - def _get_header(self, env, key, default=None): - """Get http header from environment.""" - env_key = self._header_to_env_var(key) - return env.get(env_key, default) - - def _protect_cache_value(self, token, data): - """ Encrypt or sign data if necessary. """ - try: - if self._memcache_security_strategy == 'ENCRYPT': - return memcache_crypt.encrypt_data(token, - self._memcache_secret_key, - data) - elif self._memcache_security_strategy == 'MAC': - return memcache_crypt.sign_data(token, data) - else: - return data - except: - msg = 'Failed to encrypt/sign cache data.' - self.LOG.exception(msg) - return data - - def _unprotect_cache_value(self, token, data): - """ Decrypt or verify signed data if necessary. """ - if data is None: - return data - - try: - if self._memcache_security_strategy == 'ENCRYPT': - return memcache_crypt.decrypt_data(token, - self._memcache_secret_key, - data) - elif self._memcache_security_strategy == 'MAC': - return memcache_crypt.verify_signed_data(token, data) - else: - return data - except: - msg = 'Failed to decrypt/verify cache data.' - self.LOG.exception(msg) - # this should have the same effect as data not found in cache - return None - - def _get_cache_key(self, token): - """ Return the cache key. - - Do not use clear token as key if memcache protection is on. - - """ - htoken = token - if self._memcache_security_strategy in ('ENCRYPT', 'MAC'): - derv_token = token + self._memcache_secret_key - htoken = memcache_crypt.hash_data(derv_token) - return 'tokens/%s' % htoken - - def _cache_get(self, token): - """Return token information from cache. - - If token is invalid raise InvalidUserToken - return token only if fresh (not expired). - """ - if self._cache and token: - key = self._get_cache_key(token) - cached = self._cache.get(key) - cached = self._unprotect_cache_value(token, cached) - if cached == 'invalid': - self.LOG.debug('Cached Token %s is marked unauthorized', token) - raise InvalidUserToken('Token authorization failed') - if cached: - data, expires = cached - if time.time() < float(expires): - self.LOG.debug('Returning cached token %s', token) - return data - else: - self.LOG.debug('Cached Token %s seems expired', token) - - def _cache_store(self, token, data, expires=None): - """ Store value into memcache. """ - key = self._get_cache_key(token) - data = self._protect_cache_value(token, data) - data_to_store = data - if expires: - data_to_store = (data, expires) - # we need to special-case set() because of the incompatibility between - # Swift MemcacheRing and python-memcached. See - # https://bugs.launchpad.net/swift/+bug/1095730 - if self._use_keystone_cache: - self._cache.set(key, - data_to_store, - time=self.token_cache_time) - else: - self._cache.set(key, - data_to_store, - timeout=self.token_cache_time) - - def _cache_put(self, token, data): - """ Put token data into the cache. - - Stores the parsed expire date in cache allowing - quick check of token freshness on retrieval. - """ - if self._cache and data: - if 'token' in data.get('access', {}): - timestamp = data['access']['token']['expires'] - expires = timeutils.parse_isotime(timestamp).strftime('%s') - else: - self.LOG.error('invalid token format') - return - self.LOG.debug('Storing %s token in memcache', token) - self._cache_store(token, data, expires) - - def _cache_store_invalid(self, token): - """Store invalid token in cache.""" - if self._cache: - self.LOG.debug( - 'Marking token %s as unauthorized in memcache', token) - self._cache_store(token, 'invalid') - - def cert_file_missing(self, called_proc_err, file_name): - return (called_proc_err.output.find(file_name) - and not os.path.exists(file_name)) - - def verify_uuid_token(self, user_token, retry=True): - """Authenticate user token with keystone. - - :param user_token: user's token id - :param retry: flag that forces the middleware to retry - user authentication when an indeterminate - response is received. Optional. - :return token object received from keystone on success - :raise InvalidUserToken if token is rejected - :raise ServiceError if unable to authenticate token - - """ - - headers = {'X-Auth-Token': self.get_admin_token()} - response, data = self._json_request( - 'GET', - '/v2.0/tokens/%s' % safe_quote(user_token), - additional_headers=headers) - - if response.status == 200: - self._cache_put(user_token, data) - return data - if response.status == 404: - # FIXME(ja): I'm assuming the 404 status means that user_token is - # invalid - not that the admin_token is invalid - self._cache_store_invalid(user_token) - self.LOG.warn("Authorization failed for token %s", user_token) - raise InvalidUserToken('Token authorization failed') - if response.status == 401: - self.LOG.info( - 'Keystone rejected admin token %s, resetting', headers) - self.admin_token = None - else: - self.LOG.error('Bad response code while validating token: %s' % - response.status) - if retry: - self.LOG.info('Retrying validation') - return self._validate_user_token(user_token, False) - else: - self.LOG.warn("Invalid user token: %s. Keystone response: %s.", - user_token, data) - - raise InvalidUserToken() - - def is_signed_token_revoked(self, signed_text): - """Indicate whether the token appears in the revocation list.""" - revocation_list = self.token_revocation_list - revoked_tokens = revocation_list.get('revoked', []) - if not revoked_tokens: - return - revoked_ids = (x['id'] for x in revoked_tokens) - token_id = utils.hash_signed_token(signed_text) - for revoked_id in revoked_ids: - if token_id == revoked_id: - self.LOG.debug('Token %s is marked as having been revoked', - token_id) - return True - return False - - def cms_verify(self, data): - """Verifies the signature of the provided data's IAW CMS syntax. - - If either of the certificate files are missing, fetch them and - retry. - """ - while True: - try: - output = cms.cms_verify(data, self.signing_cert_file_name, - self.ca_file_name) - except cms.subprocess.CalledProcessError as err: - if self.cert_file_missing(err, self.signing_cert_file_name): - self.fetch_signing_cert() - continue - if self.cert_file_missing(err, self.ca_file_name): - self.fetch_ca_cert() - continue - raise err - return output - - def verify_signed_token(self, signed_text): - """Check that the token is unrevoked and has a valid signature.""" - if self.is_signed_token_revoked(signed_text): - raise InvalidUserToken('Token has been revoked') - - formatted = cms.token_to_cms(signed_text) - return self.cms_verify(formatted) - - @property - def token_revocation_list_fetched_time(self): - if not self._token_revocation_list_fetched_time: - # If the fetched list has been written to disk, use its - # modification time. - if os.path.exists(self.revoked_file_name): - mtime = os.path.getmtime(self.revoked_file_name) - fetched_time = datetime.datetime.fromtimestamp(mtime) - # Otherwise the list will need to be fetched. - else: - fetched_time = datetime.datetime.min - self._token_revocation_list_fetched_time = fetched_time - return self._token_revocation_list_fetched_time - - @token_revocation_list_fetched_time.setter - def token_revocation_list_fetched_time(self, value): - self._token_revocation_list_fetched_time = value - - @property - def token_revocation_list(self): - timeout = (self.token_revocation_list_fetched_time + - self.token_revocation_list_cache_timeout) - list_is_current = timeutils.utcnow() < timeout - if list_is_current: - # Load the list from disk if required - if not self._token_revocation_list: - with open(self.revoked_file_name, 'r') as f: - self._token_revocation_list = jsonutils.loads(f.read()) - else: - self.token_revocation_list = self.fetch_revocation_list() - return self._token_revocation_list - - @token_revocation_list.setter - def token_revocation_list(self, value): - """Save a revocation list to memory and to disk. - - :param value: A json-encoded revocation list - - """ - self._token_revocation_list = jsonutils.loads(value) - self.token_revocation_list_fetched_time = timeutils.utcnow() - with open(self.revoked_file_name, 'w') as f: - f.write(value) - - def fetch_revocation_list(self, retry=True): - headers = {'X-Auth-Token': self.get_admin_token()} - response, data = self._json_request('GET', '/v2.0/tokens/revoked', - additional_headers=headers) - if response.status == 401: - if retry: - self.LOG.info( - 'Keystone rejected admin token %s, resetting admin token', - headers) - self.admin_token = None - return self.fetch_revocation_list(retry=False) - if response.status != 200: - raise ServiceError('Unable to fetch token revocation list.') - if (not 'signed' in data): - raise ServiceError('Revocation list inmproperly formatted.') - return self.cms_verify(data['signed']) - - def fetch_signing_cert(self): - response, data = self._http_request('GET', - '/v2.0/certificates/signing') - try: - #todo check response - certfile = open(self.signing_cert_file_name, 'w') - certfile.write(data) - certfile.close() - except (AssertionError, KeyError): - self.LOG.warn( - "Unexpected response from keystone service: %s", data) - raise ServiceError('invalid json response') - - def fetch_ca_cert(self): - response, data = self._http_request('GET', - '/v2.0/certificates/ca') - try: - #todo check response - certfile = open(self.ca_file_name, 'w') - certfile.write(data) - certfile.close() - except (AssertionError, KeyError): - self.LOG.warn( - "Unexpected response from keystone service: %s", data) - raise ServiceError('invalid json response') - - -def filter_factory(global_conf, **local_conf): - """Returns a WSGI filter app for use with paste.deploy.""" - conf = global_conf.copy() - conf.update(local_conf) - - def auth_filter(app): - return AuthProtocol(app, conf) - return auth_filter - - -def app_factory(global_conf, **local_conf): - conf = global_conf.copy() - conf.update(local_conf) - return AuthProtocol(None, conf) diff --git a/keystoneclient/middleware/memcache_crypt.py b/keystoneclient/middleware/memcache_crypt.py deleted file mode 100755 index 91e261da0..000000000 --- a/keystoneclient/middleware/memcache_crypt.py +++ /dev/null @@ -1,157 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2012 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Utilities for memcache encryption and integrity check. - -Data is serialized before been encrypted or MACed. Encryption have a -dependency on the pycrypto. If pycrypto is not available, -CryptoUnabailableError will be raised. - -Encrypted data stored in memcache are prefixed with '{ENCRYPT:AES256}'. - -MACed data stored in memcache are prefixed with '{MAC:SHA1}'. - -""" - -import base64 -import functools -import hashlib -import json -import os - -# make sure pycrypt is available -try: - from Crypto.Cipher import AES -except ImportError: - AES = None - - -# prefix marker indicating data is HMACed (signed by a secret key) -MAC_MARKER = '{MAC:SHA1}' -# prefix marker indicating data is encrypted -ENCRYPT_MARKER = '{ENCRYPT:AES256}' - - -class InvalidMacError(Exception): - """ raise when unable to verify MACed data - - This usually indicates that data had been expectedly modified in memcache. - - """ - pass - - -class DecryptError(Exception): - """ raise when unable to decrypt encrypted data - - """ - pass - - -class CryptoUnavailableError(Exception): - """ raise when Python Crypto module is not available - - """ - pass - - -def assert_crypto_availability(f): - """ Ensure Crypto module is available. """ - - @functools.wraps(f) - def wrapper(*args, **kwds): - if AES is None: - raise CryptoUnavailableError() - return f(*args, **kwds) - return wrapper - - -def generate_aes_key(token, secret): - """ Generates and returns a 256 bit AES key, based on sha256 hash. """ - return hashlib.sha256(token + secret).digest() - - -def compute_mac(token, serialized_data): - """ Computes and returns the base64 encoded MAC. """ - return hash_data(serialized_data + token) - - -def hash_data(data): - """ Return the base64 encoded SHA1 hash of the data. """ - return base64.b64encode(hashlib.sha1(data).digest()) - - -def sign_data(token, data): - """ MAC the data using SHA1. """ - mac_data = {} - mac_data['serialized_data'] = json.dumps(data) - mac = compute_mac(token, mac_data['serialized_data']) - mac_data['mac'] = mac - md = MAC_MARKER + base64.b64encode(json.dumps(mac_data)) - return md - - -def verify_signed_data(token, data): - """ Verify data integrity by ensuring MAC is valid. """ - if data.startswith(MAC_MARKER): - try: - data = data[len(MAC_MARKER):] - mac_data = json.loads(base64.b64decode(data)) - mac = compute_mac(token, mac_data['serialized_data']) - if mac != mac_data['mac']: - raise InvalidMacError('invalid MAC; expect=%s, actual=%s' % - (mac_data['mac'], mac)) - return json.loads(mac_data['serialized_data']) - except: - raise InvalidMacError('invalid MAC; data appeared to be corrupted') - else: - # doesn't appear to be MACed data - return data - - -@assert_crypto_availability -def encrypt_data(token, secret, data): - """ Encryptes the data with the given secret key. """ - iv = os.urandom(16) - aes_key = generate_aes_key(token, secret) - cipher = AES.new(aes_key, AES.MODE_CFB, iv) - data = json.dumps(data) - encoded_data = base64.b64encode(iv + cipher.encrypt(data)) - encoded_data = ENCRYPT_MARKER + encoded_data - return encoded_data - - -@assert_crypto_availability -def decrypt_data(token, secret, data): - """ Decrypt the data with the given secret key. """ - if data.startswith(ENCRYPT_MARKER): - try: - # encrypted data - encoded_data = data[len(ENCRYPT_MARKER):] - aes_key = generate_aes_key(token, secret) - decoded_data = base64.b64decode(encoded_data) - iv = decoded_data[:16] - encrypted_data = decoded_data[16:] - cipher = AES.new(aes_key, AES.MODE_CFB, iv) - decrypted_data = cipher.decrypt(encrypted_data) - return json.loads(decrypted_data) - except: - raise DecryptError('data appeared to be corrupted') - else: - # doesn't appear to be encrypted data - return data diff --git a/keystoneclient/middleware/test.py b/keystoneclient/middleware/test.py deleted file mode 100644 index e5c117120..000000000 --- a/keystoneclient/middleware/test.py +++ /dev/null @@ -1,67 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# -# Test support for middleware authentication -# - -import os -import sys - - -ROOTDIR = os.path.dirname(os.path.abspath(os.curdir)) - - -def rootdir(*p): - return os.path.join(ROOTDIR, *p) - - -class NoModule(object): - """A mixin class to provide support for unloading/disabling modules.""" - - def __init__(self, *args, **kw): - super(NoModule, self).__init__(*args, **kw) - self._finders = [] - self._cleared_modules = {} - - def tearDown(self): - super(NoModule, self).tearDown() - for finder in self._finders: - sys.meta_path.remove(finder) - sys.modules.update(self._cleared_modules) - - def clear_module(self, module): - cleared_modules = {} - for fullname in sys.modules.keys(): - if fullname == module or fullname.startswith(module + '.'): - cleared_modules[fullname] = sys.modules.pop(fullname) - return cleared_modules - - def disable_module(self, module): - """Ensure ImportError for the specified module.""" - - # Clear 'module' references in sys.modules - self._cleared_modules.update(self.clear_module(module)) - - # Disallow further imports of 'module' - class NoModule(object): - def find_module(self, fullname, path): - if fullname == module or fullname.startswith(module + '.'): - raise ImportError - - finder = NoModule() - self._finders.append(finder) - sys.meta_path.insert(0, finder) diff --git a/keystoneclient/openstack/common/cfg.py b/keystoneclient/openstack/common/cfg.py deleted file mode 100644 index d48eb8639..000000000 --- a/keystoneclient/openstack/common/cfg.py +++ /dev/null @@ -1,1653 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Red Hat, Inc. -# -# 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. - -r""" -Configuration options which may be set on the command line or in config files. - -The schema for each option is defined using the Opt sub-classes, e.g.: - -:: - - common_opts = [ - cfg.StrOpt('bind_host', - default='0.0.0.0', - help='IP address to listen on'), - cfg.IntOpt('bind_port', - default=9292, - help='Port number to listen on') - ] - -Options can be strings, integers, floats, booleans, lists or 'multi strings':: - - enabled_apis_opt = cfg.ListOpt('enabled_apis', - default=['ec2', 'osapi_compute'], - help='List of APIs to enable by default') - - DEFAULT_EXTENSIONS = [ - 'nova.api.openstack.compute.contrib.standard_extensions' - ] - osapi_compute_extension_opt = cfg.MultiStrOpt('osapi_compute_extension', - default=DEFAULT_EXTENSIONS) - -Option schemas are registered with the config manager at runtime, but before -the option is referenced:: - - class ExtensionManager(object): - - enabled_apis_opt = cfg.ListOpt(...) - - def __init__(self, conf): - self.conf = conf - self.conf.register_opt(enabled_apis_opt) - ... - - def _load_extensions(self): - for ext_factory in self.conf.osapi_compute_extension: - .... - -A common usage pattern is for each option schema to be defined in the module or -class which uses the option:: - - opts = ... - - def add_common_opts(conf): - conf.register_opts(opts) - - def get_bind_host(conf): - return conf.bind_host - - def get_bind_port(conf): - return conf.bind_port - -An option may optionally be made available via the command line. Such options -must registered with the config manager before the command line is parsed (for -the purposes of --help and CLI arg validation):: - - cli_opts = [ - cfg.BoolOpt('verbose', - short='v', - default=False, - help='Print more verbose output'), - cfg.BoolOpt('debug', - short='d', - default=False, - help='Print debugging output'), - ] - - def add_common_opts(conf): - conf.register_cli_opts(cli_opts) - -The config manager has two CLI options defined by default, --config-file -and --config-dir:: - - class ConfigOpts(object): - - def __call__(self, ...): - - opts = [ - MultiStrOpt('config-file', - ...), - StrOpt('config-dir', - ...), - ] - - self.register_cli_opts(opts) - -Option values are parsed from any supplied config files using -openstack.common.iniparser. If none are specified, a default set is used -e.g. glance-api.conf and glance-common.conf:: - - glance-api.conf: - [DEFAULT] - bind_port = 9292 - - glance-common.conf: - [DEFAULT] - bind_host = 0.0.0.0 - -Option values in config files override those on the command line. Config files -are parsed in order, with values in later files overriding those in earlier -files. - -The parsing of CLI args and config files is initiated by invoking the config -manager e.g.:: - - conf = ConfigOpts() - conf.register_opt(BoolOpt('verbose', ...)) - conf(sys.argv[1:]) - if conf.verbose: - ... - -Options can be registered as belonging to a group:: - - rabbit_group = cfg.OptGroup(name='rabbit', - title='RabbitMQ options') - - rabbit_host_opt = cfg.StrOpt('host', - default='localhost', - help='IP/hostname to listen on'), - rabbit_port_opt = cfg.IntOpt('port', - default=5672, - help='Port number to listen on') - - def register_rabbit_opts(conf): - conf.register_group(rabbit_group) - # options can be registered under a group in either of these ways: - conf.register_opt(rabbit_host_opt, group=rabbit_group) - conf.register_opt(rabbit_port_opt, group='rabbit') - -If it no group attributes are required other than the group name, the group -need not be explicitly registered e.g. - - def register_rabbit_opts(conf): - # The group will automatically be created, equivalent calling:: - # conf.register_group(OptGroup(name='rabbit')) - conf.register_opt(rabbit_port_opt, group='rabbit') - -If no group is specified, options belong to the 'DEFAULT' section of config -files:: - - glance-api.conf: - [DEFAULT] - bind_port = 9292 - ... - - [rabbit] - host = localhost - port = 5672 - use_ssl = False - userid = guest - password = guest - virtual_host = / - -Command-line options in a group are automatically prefixed with the -group name:: - - --rabbit-host localhost --rabbit-port 9999 - -Option values in the default group are referenced as attributes/properties on -the config manager; groups are also attributes on the config manager, with -attributes for each of the options associated with the group:: - - server.start(app, conf.bind_port, conf.bind_host, conf) - - self.connection = kombu.connection.BrokerConnection( - hostname=conf.rabbit.host, - port=conf.rabbit.port, - ...) - -Option values may reference other values using PEP 292 string substitution:: - - opts = [ - cfg.StrOpt('state_path', - default=os.path.join(os.path.dirname(__file__), '../'), - help='Top-level directory for maintaining nova state'), - cfg.StrOpt('sqlite_db', - default='nova.sqlite', - help='file name for sqlite'), - cfg.StrOpt('sql_connection', - default='sqlite:///$state_path/$sqlite_db', - help='connection string for sql database'), - ] - -Note that interpolation can be avoided by using '$$'. - -For command line utilities that dispatch to other command line utilities, the -disable_interspersed_args() method is available. If this this method is called, -then parsing e.g.:: - - script --verbose cmd --debug /tmp/mything - -will no longer return:: - - ['cmd', '/tmp/mything'] - -as the leftover arguments, but will instead return:: - - ['cmd', '--debug', '/tmp/mything'] - -i.e. argument parsing is stopped at the first non-option argument. - -Options may be declared as required so that an error is raised if the user -does not supply a value for the option. - -Options may be declared as secret so that their values are not leaked into -log files: - - opts = [ - cfg.StrOpt('s3_store_access_key', secret=True), - cfg.StrOpt('s3_store_secret_key', secret=True), - ... - ] - -This module also contains a global instance of the CommonConfigOpts class -in order to support a common usage pattern in OpenStack: - - from keystoneclient.openstack.common import cfg - - opts = [ - cfg.StrOpt('bind_host', default='0.0.0.0'), - cfg.IntOpt('bind_port', default=9292), - ] - - CONF = cfg.CONF - CONF.register_opts(opts) - - def start(server, app): - server.start(app, CONF.bind_port, CONF.bind_host) - -""" - -import collections -import copy -import functools -import glob -import optparse -import os -import string -import sys - -from keystoneclient.openstack.common import iniparser - - -class Error(Exception): - """Base class for cfg exceptions.""" - - def __init__(self, msg=None): - self.msg = msg - - def __str__(self): - return self.msg - - -class ArgsAlreadyParsedError(Error): - """Raised if a CLI opt is registered after parsing.""" - - def __str__(self): - ret = "arguments already parsed" - if self.msg: - ret += ": " + self.msg - return ret - - -class NoSuchOptError(Error, AttributeError): - """Raised if an opt which doesn't exist is referenced.""" - - def __init__(self, opt_name, group=None): - self.opt_name = opt_name - self.group = group - - def __str__(self): - if self.group is None: - return "no such option: %s" % self.opt_name - else: - return "no such option in group %s: %s" % (self.group.name, - self.opt_name) - - -class NoSuchGroupError(Error): - """Raised if a group which doesn't exist is referenced.""" - - def __init__(self, group_name): - self.group_name = group_name - - def __str__(self): - return "no such group: %s" % self.group_name - - -class DuplicateOptError(Error): - """Raised if multiple opts with the same name are registered.""" - - def __init__(self, opt_name): - self.opt_name = opt_name - - def __str__(self): - return "duplicate option: %s" % self.opt_name - - -class RequiredOptError(Error): - """Raised if an option is required but no value is supplied by the user.""" - - def __init__(self, opt_name, group=None): - self.opt_name = opt_name - self.group = group - - def __str__(self): - if self.group is None: - return "value required for option: %s" % self.opt_name - else: - return "value required for option: %s.%s" % (self.group.name, - self.opt_name) - - -class TemplateSubstitutionError(Error): - """Raised if an error occurs substituting a variable in an opt value.""" - - def __str__(self): - return "template substitution error: %s" % self.msg - - -class ConfigFilesNotFoundError(Error): - """Raised if one or more config files are not found.""" - - def __init__(self, config_files): - self.config_files = config_files - - def __str__(self): - return ('Failed to read some config files: %s' % - string.join(self.config_files, ',')) - - -class ConfigFileParseError(Error): - """Raised if there is an error parsing a config file.""" - - def __init__(self, config_file, msg): - self.config_file = config_file - self.msg = msg - - def __str__(self): - return 'Failed to parse %s: %s' % (self.config_file, self.msg) - - -class ConfigFileValueError(Error): - """Raised if a config file value does not match its opt type.""" - pass - - -def _fixpath(p): - """Apply tilde expansion and absolutization to a path.""" - return os.path.abspath(os.path.expanduser(p)) - - -def _get_config_dirs(project=None): - """Return a list of directors where config files may be located. - - :param project: an optional project name - - If a project is specified, following directories are returned:: - - ~/.${project}/ - ~/ - /etc/${project}/ - /etc/ - - Otherwise, these directories:: - - ~/ - /etc/ - """ - cfg_dirs = [ - _fixpath(os.path.join('~', '.' + project)) if project else None, - _fixpath('~'), - os.path.join('/etc', project) if project else None, - '/etc' - ] - - return filter(bool, cfg_dirs) - - -def _search_dirs(dirs, basename, extension=""): - """Search a list of directories for a given filename. - - Iterator over the supplied directories, returning the first file - found with the supplied name and extension. - - :param dirs: a list of directories - :param basename: the filename, e.g. 'glance-api' - :param extension: the file extension, e.g. '.conf' - :returns: the path to a matching file, or None - """ - for d in dirs: - path = os.path.join(d, '%s%s' % (basename, extension)) - if os.path.exists(path): - return path - - -def find_config_files(project=None, prog=None, extension='.conf'): - """Return a list of default configuration files. - - :param project: an optional project name - :param prog: the program name, defaulting to the basename of sys.argv[0] - :param extension: the type of the config file - - We default to two config files: [${project}.conf, ${prog}.conf] - - And we look for those config files in the following directories:: - - ~/.${project}/ - ~/ - /etc/${project}/ - /etc/ - - We return an absolute path for (at most) one of each the default config - files, for the topmost directory it exists in. - - For example, if project=foo, prog=bar and /etc/foo/foo.conf, /etc/bar.conf - and ~/.foo/bar.conf all exist, then we return ['/etc/foo/foo.conf', - '~/.foo/bar.conf'] - - If no project name is supplied, we only look for ${prog.conf}. - """ - if prog is None: - prog = os.path.basename(sys.argv[0]) - - cfg_dirs = _get_config_dirs(project) - - config_files = [] - if project: - config_files.append(_search_dirs(cfg_dirs, project, extension)) - config_files.append(_search_dirs(cfg_dirs, prog, extension)) - - return filter(bool, config_files) - - -def _is_opt_registered(opts, opt): - """Check whether an opt with the same name is already registered. - - The same opt may be registered multiple times, with only the first - registration having any effect. However, it is an error to attempt - to register a different opt with the same name. - - :param opts: the set of opts already registered - :param opt: the opt to be registered - :returns: True if the opt was previously registered, False otherwise - :raises: DuplicateOptError if a naming conflict is detected - """ - if opt.dest in opts: - if opts[opt.dest]['opt'] != opt: - raise DuplicateOptError(opt.name) - return True - else: - return False - - -class Opt(object): - - """Base class for all configuration options. - - An Opt object has no public methods, but has a number of public string - properties: - - name: - the name of the option, which may include hyphens - dest: - the (hyphen-less) ConfigOpts property which contains the option value - short: - a single character CLI option name - default: - the default value of the option - metavar: - the name shown as the argument to a CLI option in --help output - help: - an string explaining how the options value is used - """ - multi = False - - def __init__(self, name, dest=None, short=None, default=None, - metavar=None, help=None, secret=False, required=False, - deprecated_name=None): - """Construct an Opt object. - - The only required parameter is the option's name. However, it is - common to also supply a default and help string for all options. - - :param name: the option's name - :param dest: the name of the corresponding ConfigOpts property - :param short: a single character CLI option name - :param default: the default value of the option - :param metavar: the option argument to show in --help - :param help: an explanation of how the option is used - :param secret: true iff the value should be obfuscated in log output - :param required: true iff a value must be supplied for this option - :param deprecated_name: deprecated name option. Acts like an alias - """ - self.name = name - if dest is None: - self.dest = self.name.replace('-', '_') - else: - self.dest = dest - self.short = short - self.default = default - self.metavar = metavar - self.help = help - self.secret = secret - self.required = required - if deprecated_name is not None: - self.deprecated_name = deprecated_name.replace('-', '_') - else: - self.deprecated_name = None - - def __ne__(self, another): - return vars(self) != vars(another) - - def _get_from_config_parser(self, cparser, section): - """Retrieves the option value from a MultiConfigParser object. - - This is the method ConfigOpts uses to look up the option value from - config files. Most opt types override this method in order to perform - type appropriate conversion of the returned value. - - :param cparser: a ConfigParser object - :param section: a section name - """ - return self._cparser_get_with_deprecated(cparser, section) - - def _cparser_get_with_deprecated(self, cparser, section): - """If cannot find option as dest try deprecated_name alias.""" - if self.deprecated_name is not None: - return cparser.get(section, [self.dest, self.deprecated_name]) - return cparser.get(section, [self.dest]) - - def _add_to_cli(self, parser, group=None): - """Makes the option available in the command line interface. - - This is the method ConfigOpts uses to add the opt to the CLI interface - as appropriate for the opt type. Some opt types may extend this method, - others may just extend the helper methods it uses. - - :param parser: the CLI option parser - :param group: an optional OptGroup object - """ - container = self._get_optparse_container(parser, group) - kwargs = self._get_optparse_kwargs(group) - prefix = self._get_optparse_prefix('', group) - self._add_to_optparse(container, self.name, self.short, kwargs, prefix, - self.deprecated_name) - - def _add_to_optparse(self, container, name, short, kwargs, prefix='', - deprecated_name=None): - """Add an option to an optparse parser or group. - - :param container: an optparse.OptionContainer object - :param name: the opt name - :param short: the short opt name - :param kwargs: the keyword arguments for add_option() - :param prefix: an optional prefix to prepend to the opt name - :raises: DuplicateOptError if a naming confict is detected - """ - args = ['--' + prefix + name] - if short: - args += ['-' + short] - if deprecated_name: - args += ['--' + prefix + deprecated_name] - for a in args: - if container.has_option(a): - raise DuplicateOptError(a) - container.add_option(*args, **kwargs) - - def _get_optparse_container(self, parser, group): - """Returns an optparse.OptionContainer. - - :param parser: an optparse.OptionParser - :param group: an (optional) OptGroup object - :returns: an optparse.OptionGroup if a group is given, else the parser - """ - if group is not None: - return group._get_optparse_group(parser) - else: - return parser - - def _get_optparse_kwargs(self, group, **kwargs): - """Build a dict of keyword arguments for optparse's add_option(). - - Most opt types extend this method to customize the behaviour of the - options added to optparse. - - :param group: an optional group - :param kwargs: optional keyword arguments to add to - :returns: a dict of keyword arguments - """ - dest = self.dest - if group is not None: - dest = group.name + '_' + dest - kwargs.update({'dest': dest, - 'metavar': self.metavar, - 'help': self.help, }) - return kwargs - - def _get_optparse_prefix(self, prefix, group): - """Build a prefix for the CLI option name, if required. - - CLI options in a group are prefixed with the group's name in order - to avoid conflicts between similarly named options in different - groups. - - :param prefix: an existing prefix to append to (e.g. 'no' or '') - :param group: an optional OptGroup object - :returns: a CLI option prefix including the group name, if appropriate - """ - if group is not None: - return group.name + '-' + prefix - else: - return prefix - - -class StrOpt(Opt): - """ - String opts do not have their values transformed and are returned as - str objects. - """ - pass - - -class BoolOpt(Opt): - - """ - Bool opts are set to True or False on the command line using --optname or - --noopttname respectively. - - In config files, boolean values are case insensitive and can be set using - 1/0, yes/no, true/false or on/off. - """ - - _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, - '0': False, 'no': False, 'false': False, 'off': False} - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a boolean from ConfigParser.""" - def convert_bool(v): - value = self._boolean_states.get(v.lower()) - if value is None: - raise ValueError('Unexpected boolean value %r' % v) - - return value - - return [convert_bool(v) for v in - self._cparser_get_with_deprecated(cparser, section)] - - def _add_to_cli(self, parser, group=None): - """Extends the base class method to add the --nooptname option.""" - super(BoolOpt, self)._add_to_cli(parser, group) - self._add_inverse_to_optparse(parser, group) - - def _add_inverse_to_optparse(self, parser, group): - """Add the --nooptname option to the option parser.""" - container = self._get_optparse_container(parser, group) - kwargs = self._get_optparse_kwargs(group, action='store_false') - prefix = self._get_optparse_prefix('no', group) - kwargs["help"] = "The inverse of --" + self.name - self._add_to_optparse(container, self.name, None, kwargs, prefix, - self.deprecated_name) - - def _get_optparse_kwargs(self, group, action='store_true', **kwargs): - """Extends the base optparse keyword dict for boolean options.""" - return super(BoolOpt, - self)._get_optparse_kwargs(group, action=action, **kwargs) - - -class IntOpt(Opt): - - """Int opt values are converted to integers using the int() builtin.""" - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a integer from ConfigParser.""" - return [int(v) for v in self._cparser_get_with_deprecated(cparser, - section)] - - def _get_optparse_kwargs(self, group, **kwargs): - """Extends the base optparse keyword dict for integer options.""" - return super(IntOpt, - self)._get_optparse_kwargs(group, type='int', **kwargs) - - -class FloatOpt(Opt): - - """Float opt values are converted to floats using the float() builtin.""" - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a float from ConfigParser.""" - return [float(v) for v in - self._cparser_get_with_deprecated(cparser, section)] - - def _get_optparse_kwargs(self, group, **kwargs): - """Extends the base optparse keyword dict for float options.""" - return super(FloatOpt, - self)._get_optparse_kwargs(group, type='float', **kwargs) - - -class ListOpt(Opt): - - """ - List opt values are simple string values separated by commas. The opt value - is a list containing these strings. - """ - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a list from ConfigParser.""" - return [v.split(',') for v in - self._cparser_get_with_deprecated(cparser, section)] - - def _get_optparse_kwargs(self, group, **kwargs): - """Extends the base optparse keyword dict for list options.""" - return super(ListOpt, - self)._get_optparse_kwargs(group, - type='string', - action='callback', - callback=self._parse_list, - **kwargs) - - def _parse_list(self, option, opt, value, parser): - """An optparse callback for parsing an option value into a list.""" - setattr(parser.values, self.dest, value.split(',')) - - -class MultiStrOpt(Opt): - - """ - Multistr opt values are string opts which may be specified multiple times. - The opt value is a list containing all the string values specified. - """ - multi = True - - def _get_optparse_kwargs(self, group, **kwargs): - """Extends the base optparse keyword dict for multi str options.""" - return super(MultiStrOpt, - self)._get_optparse_kwargs(group, action='append') - - def _cparser_get_with_deprecated(self, cparser, section): - """If cannot find option as dest try deprecated_name alias.""" - if self.deprecated_name is not None: - return cparser.get(section, [self.dest, self.deprecated_name], - multi=True) - return cparser.get(section, [self.dest], multi=True) - - -class OptGroup(object): - - """ - Represents a group of opts. - - CLI opts in the group are automatically prefixed with the group name. - - Each group corresponds to a section in config files. - - An OptGroup object has no public methods, but has a number of public string - properties: - - name: - the name of the group - title: - the group title as displayed in --help - help: - the group description as displayed in --help - """ - - def __init__(self, name, title=None, help=None): - """Constructs an OptGroup object. - - :param name: the group name - :param title: the group title for --help - :param help: the group description for --help - """ - self.name = name - if title is None: - self.title = "%s options" % title - else: - self.title = title - self.help = help - - self._opts = {} # dict of dicts of (opt:, override:, default:) - self._optparse_group = None - - def _register_opt(self, opt): - """Add an opt to this group. - - :param opt: an Opt object - :returns: False if previously registered, True otherwise - :raises: DuplicateOptError if a naming conflict is detected - """ - if _is_opt_registered(self._opts, opt): - return False - - self._opts[opt.dest] = {'opt': opt} - - return True - - def _unregister_opt(self, opt): - """Remove an opt from this group. - - :param opt: an Opt object - """ - if opt.dest in self._opts: - del self._opts[opt.dest] - - def _get_optparse_group(self, parser): - """Build an optparse.OptionGroup for this group.""" - if self._optparse_group is None: - self._optparse_group = optparse.OptionGroup(parser, self.title, - self.help) - return self._optparse_group - - def _clear(self): - """Clear this group's option parsing state.""" - self._optparse_group = None - - -class ParseError(iniparser.ParseError): - def __init__(self, msg, lineno, line, filename): - super(ParseError, self).__init__(msg, lineno, line) - self.filename = filename - - def __str__(self): - return 'at %s:%d, %s: %r' % (self.filename, self.lineno, - self.msg, self.line) - - -class ConfigParser(iniparser.BaseParser): - def __init__(self, filename, sections): - super(ConfigParser, self).__init__() - self.filename = filename - self.sections = sections - self.section = None - - def parse(self): - with open(self.filename) as f: - return super(ConfigParser, self).parse(f) - - def new_section(self, section): - self.section = section - self.sections.setdefault(self.section, {}) - - def assignment(self, key, value): - if not self.section: - raise self.error_no_section() - - self.sections[self.section].setdefault(key, []) - self.sections[self.section][key].append('\n'.join(value)) - - def parse_exc(self, msg, lineno, line=None): - return ParseError(msg, lineno, line, self.filename) - - def error_no_section(self): - return self.parse_exc('Section must be started before assignment', - self.lineno) - - -class MultiConfigParser(object): - def __init__(self): - self.parsed = [] - - def read(self, config_files): - read_ok = [] - - for filename in config_files: - sections = {} - parser = ConfigParser(filename, sections) - - try: - parser.parse() - except IOError: - continue - self.parsed.insert(0, sections) - read_ok.append(filename) - - return read_ok - - def get(self, section, names, multi=False): - rvalue = [] - for sections in self.parsed: - if section not in sections: - continue - for name in names: - if name in sections[section]: - if multi: - rvalue = sections[section][name] + rvalue - else: - return sections[section][name] - if multi and rvalue != []: - return rvalue - raise KeyError - - -class ConfigOpts(collections.Mapping): - - """ - Config options which may be set on the command line or in config files. - - ConfigOpts is a configuration option manager with APIs for registering - option schemas, grouping options, parsing option values and retrieving - the values of options. - """ - - def __init__(self): - """Construct a ConfigOpts object.""" - self._opts = {} # dict of dicts of (opt:, override:, default:) - self._groups = {} - - self._args = None - self._oparser = None - self._cparser = None - self._cli_values = {} - self.__cache = {} - self._config_opts = [] - self._disable_interspersed_args = False - - def _setup(self, project, prog, version, usage, default_config_files): - """Initialize a ConfigOpts object for option parsing.""" - if prog is None: - prog = os.path.basename(sys.argv[0]) - - if default_config_files is None: - default_config_files = find_config_files(project, prog) - - self._oparser = optparse.OptionParser(prog=prog, - version=version, - usage=usage) - if self._disable_interspersed_args: - self._oparser.disable_interspersed_args() - - self._config_opts = [ - MultiStrOpt('config-file', - default=default_config_files, - metavar='PATH', - help='Path to a config file to use. Multiple config ' - 'files can be specified, with values in later ' - 'files taking precedence. The default files ' - ' used are: %s' % (default_config_files, )), - StrOpt('config-dir', - metavar='DIR', - help='Path to a config directory to pull *.conf ' - 'files from. This file set is sorted, so as to ' - 'provide a predictable parse order if individual ' - 'options are over-ridden. The set is parsed after ' - 'the file(s), if any, specified via --config-file, ' - 'hence over-ridden options in the directory take ' - 'precedence.'), - ] - self.register_cli_opts(self._config_opts) - - self.project = project - self.prog = prog - self.version = version - self.usage = usage - self.default_config_files = default_config_files - - def __clear_cache(f): - @functools.wraps(f) - def __inner(self, *args, **kwargs): - if kwargs.pop('clear_cache', True): - self.__cache.clear() - return f(self, *args, **kwargs) - - return __inner - - def __call__(self, - args=None, - project=None, - prog=None, - version=None, - usage=None, - default_config_files=None): - """Parse command line arguments and config files. - - Calling a ConfigOpts object causes the supplied command line arguments - and config files to be parsed, causing opt values to be made available - as attributes of the object. - - The object may be called multiple times, each time causing the previous - set of values to be overwritten. - - Automatically registers the --config-file option with either a supplied - list of default config files, or a list from find_config_files(). - - If the --config-dir option is set, any *.conf files from this - directory are pulled in, after all the file(s) specified by the - --config-file option. - - :param args: command line arguments (defaults to sys.argv[1:]) - :param project: the toplevel project name, used to locate config files - :param prog: the name of the program (defaults to sys.argv[0] basename) - :param version: the program version (for --version) - :param usage: a usage string (%prog will be expanded) - :param default_config_files: config files to use by default - :returns: the list of arguments left over after parsing options - :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError, - RequiredOptError, DuplicateOptError - """ - self.clear() - - self._setup(project, prog, version, usage, default_config_files) - - self._cli_values, leftovers = self._parse_cli_opts(args) - - self._parse_config_files() - - self._check_required_opts() - - return leftovers - - def __getattr__(self, name): - """Look up an option value and perform string substitution. - - :param name: the opt name (or 'dest', more precisely) - :returns: the option value (after string subsititution) or a GroupAttr - :raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError - """ - return self._get(name) - - def __getitem__(self, key): - """Look up an option value and perform string substitution.""" - return self.__getattr__(key) - - def __contains__(self, key): - """Return True if key is the name of a registered opt or group.""" - return key in self._opts or key in self._groups - - def __iter__(self): - """Iterate over all registered opt and group names.""" - for key in self._opts.keys() + self._groups.keys(): - yield key - - def __len__(self): - """Return the number of options and option groups.""" - return len(self._opts) + len(self._groups) - - def reset(self): - """Clear the object state and unset overrides and defaults.""" - self._unset_defaults_and_overrides() - self.clear() - - @__clear_cache - def clear(self): - """Clear the state of the object to before it was called.""" - self._args = None - self._cli_values.clear() - self._oparser = None - self._cparser = None - self.unregister_opts(self._config_opts) - for group in self._groups.values(): - group._clear() - - @__clear_cache - def register_opt(self, opt, group=None): - """Register an option schema. - - Registering an option schema makes any option value which is previously - or subsequently parsed from the command line or config files available - as an attribute of this object. - - :param opt: an instance of an Opt sub-class - :param group: an optional OptGroup object or group name - :return: False if the opt was already register, True otherwise - :raises: DuplicateOptError - """ - if group is not None: - return self._get_group(group, autocreate=True)._register_opt(opt) - - if _is_opt_registered(self._opts, opt): - return False - - self._opts[opt.dest] = {'opt': opt} - - return True - - @__clear_cache - def register_opts(self, opts, group=None): - """Register multiple option schemas at once.""" - for opt in opts: - self.register_opt(opt, group, clear_cache=False) - - @__clear_cache - def register_cli_opt(self, opt, group=None): - """Register a CLI option schema. - - CLI option schemas must be registered before the command line and - config files are parsed. This is to ensure that all CLI options are - show in --help and option validation works as expected. - - :param opt: an instance of an Opt sub-class - :param group: an optional OptGroup object or group name - :return: False if the opt was already register, True otherwise - :raises: DuplicateOptError, ArgsAlreadyParsedError - """ - if self._args is not None: - raise ArgsAlreadyParsedError("cannot register CLI option") - - return self.register_opt(opt, group, clear_cache=False) - - @__clear_cache - def register_cli_opts(self, opts, group=None): - """Register multiple CLI option schemas at once.""" - for opt in opts: - self.register_cli_opt(opt, group, clear_cache=False) - - def register_group(self, group): - """Register an option group. - - An option group must be registered before options can be registered - with the group. - - :param group: an OptGroup object - """ - if group.name in self._groups: - return - - self._groups[group.name] = copy.copy(group) - - @__clear_cache - def unregister_opt(self, opt, group=None): - """Unregister an option. - - :param opt: an Opt object - :param group: an optional OptGroup object or group name - :raises: ArgsAlreadyParsedError, NoSuchGroupError - """ - if self._args is not None: - raise ArgsAlreadyParsedError("reset before unregistering options") - - if group is not None: - self._get_group(group)._unregister_opt(opt) - elif opt.dest in self._opts: - del self._opts[opt.dest] - - @__clear_cache - def unregister_opts(self, opts, group=None): - """Unregister multiple CLI option schemas at once.""" - for opt in opts: - self.unregister_opt(opt, group, clear_cache=False) - - def import_opt(self, name, module_str, group=None): - """Import an option definition from a module. - - Import a module and check that a given option is registered. - - This is intended for use with global configuration objects - like cfg.CONF where modules commonly register options with - CONF at module load time. If one module requires an option - defined by another module it can use this method to explicitly - declare the dependency. - - :param name: the name/dest of the opt - :param module_str: the name of a module to import - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - __import__(module_str) - self._get_opt_info(name, group) - - @__clear_cache - def set_override(self, name, override, group=None): - """Override an opt value. - - Override the command line, config file and default values of a - given option. - - :param name: the name/dest of the opt - :param override: the override value - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info['override'] = override - - @__clear_cache - def set_default(self, name, default, group=None): - """Override an opt's default value. - - Override the default value of given option. A command line or - config file value will still take precedence over this default. - - :param name: the name/dest of the opt - :param default: the default value - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info['default'] = default - - @__clear_cache - def clear_override(self, name, group=None): - """Clear an override an opt value. - - Clear a previously set override of the command line, config file - and default values of a given option. - - :param name: the name/dest of the opt - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info.pop('override', None) - - @__clear_cache - def clear_default(self, name, group=None): - """Clear an override an opt's default value. - - Clear a previously set override of the default value of given option. - - :param name: the name/dest of the opt - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info.pop('default', None) - - def _all_opt_infos(self): - """A generator function for iteration opt infos.""" - for info in self._opts.values(): - yield info, None - for group in self._groups.values(): - for info in group._opts.values(): - yield info, group - - def _all_opts(self): - """A generator function for iteration opts.""" - for info, group in self._all_opt_infos(): - yield info['opt'], group - - def _unset_defaults_and_overrides(self): - """Unset any default or override on all options.""" - for info, group in self._all_opt_infos(): - info.pop('default', None) - info.pop('override', None) - - def disable_interspersed_args(self): - """Set parsing to stop on the first non-option. - - If this this method is called, then parsing e.g. - - script --verbose cmd --debug /tmp/mything - - will no longer return: - - ['cmd', '/tmp/mything'] - - as the leftover arguments, but will instead return: - - ['cmd', '--debug', '/tmp/mything'] - - i.e. argument parsing is stopped at the first non-option argument. - """ - self._disable_interspersed_args = True - - def enable_interspersed_args(self): - """Set parsing to not stop on the first non-option. - - This it the default behaviour.""" - self._disable_interspersed_args = False - - def find_file(self, name): - """Locate a file located alongside the config files. - - Search for a file with the supplied basename in the directories - which we have already loaded config files from and other known - configuration directories. - - The directory, if any, supplied by the config_dir option is - searched first. Then the config_file option is iterated over - and each of the base directories of the config_files values - are searched. Failing both of these, the standard directories - searched by the module level find_config_files() function is - used. The first matching file is returned. - - :param basename: the filename, e.g. 'policy.json' - :returns: the path to a matching file, or None - """ - dirs = [] - if self.config_dir: - dirs.append(_fixpath(self.config_dir)) - - for cf in reversed(self.config_file): - dirs.append(os.path.dirname(_fixpath(cf))) - - dirs.extend(_get_config_dirs(self.project)) - - return _search_dirs(dirs, name) - - def log_opt_values(self, logger, lvl): - """Log the value of all registered opts. - - It's often useful for an app to log its configuration to a log file at - startup for debugging. This method dumps to the entire config state to - the supplied logger at a given log level. - - :param logger: a logging.Logger object - :param lvl: the log level (e.g. logging.DEBUG) arg to logger.log() - """ - logger.log(lvl, "*" * 80) - logger.log(lvl, "Configuration options gathered from:") - logger.log(lvl, "command line args: %s", self._args) - logger.log(lvl, "config files: %s", self.config_file) - logger.log(lvl, "=" * 80) - - def _sanitize(opt, value): - """Obfuscate values of options declared secret""" - return value if not opt.secret else '*' * len(str(value)) - - for opt_name in sorted(self._opts): - opt = self._get_opt_info(opt_name)['opt'] - logger.log(lvl, "%-30s = %s", opt_name, - _sanitize(opt, getattr(self, opt_name))) - - for group_name in self._groups: - group_attr = self.GroupAttr(self, self._get_group(group_name)) - for opt_name in sorted(self._groups[group_name]._opts): - opt = self._get_opt_info(opt_name, group_name)['opt'] - logger.log(lvl, "%-30s = %s", - "%s.%s" % (group_name, opt_name), - _sanitize(opt, getattr(group_attr, opt_name))) - - logger.log(lvl, "*" * 80) - - def print_usage(self, file=None): - """Print the usage message for the current program.""" - self._oparser.print_usage(file) - - def print_help(self, file=None): - """Print the help message for the current program.""" - self._oparser.print_help(file) - - def _get(self, name, group=None): - if isinstance(group, OptGroup): - key = (group.name, name) - else: - key = (group, name) - try: - return self.__cache[key] - except KeyError: - value = self._substitute(self._do_get(name, group)) - self.__cache[key] = value - return value - - def _do_get(self, name, group=None): - """Look up an option value. - - :param name: the opt name (or 'dest', more precisely) - :param group: an OptGroup - :returns: the option value, or a GroupAttr object - :raises: NoSuchOptError, NoSuchGroupError, ConfigFileValueError, - TemplateSubstitutionError - """ - if group is None and name in self._groups: - return self.GroupAttr(self, self._get_group(name)) - - info = self._get_opt_info(name, group) - opt = info['opt'] - - if 'override' in info: - return info['override'] - - values = [] - if self._cparser is not None: - section = group.name if group is not None else 'DEFAULT' - try: - value = opt._get_from_config_parser(self._cparser, section) - except KeyError: - pass - except ValueError as ve: - raise ConfigFileValueError(str(ve)) - else: - if not opt.multi: - # No need to continue since the last value wins - return value[-1] - values.extend(value) - - name = name if group is None else group.name + '_' + name - value = self._cli_values.get(name) - if value is not None: - if not opt.multi: - return value - - return value + values - - if values: - return values - - if 'default' in info: - return info['default'] - - return opt.default - - def _substitute(self, value): - """Perform string template substitution. - - Substitute any template variables (e.g. $foo, ${bar}) in the supplied - string value(s) with opt values. - - :param value: the string value, or list of string values - :returns: the substituted string(s) - """ - if isinstance(value, list): - return [self._substitute(i) for i in value] - elif isinstance(value, str): - tmpl = string.Template(value) - return tmpl.safe_substitute(self.StrSubWrapper(self)) - else: - return value - - def _get_group(self, group_or_name, autocreate=False): - """Looks up a OptGroup object. - - Helper function to return an OptGroup given a parameter which can - either be the group's name or an OptGroup object. - - The OptGroup object returned is from the internal dict of OptGroup - objects, which will be a copy of any OptGroup object that users of - the API have access to. - - :param group_or_name: the group's name or the OptGroup object itself - :param autocreate: whether to auto-create the group if it's not found - :raises: NoSuchGroupError - """ - group = group_or_name if isinstance(group_or_name, OptGroup) else None - group_name = group.name if group else group_or_name - - if not group_name in self._groups: - if not group is None or not autocreate: - raise NoSuchGroupError(group_name) - - self.register_group(OptGroup(name=group_name)) - - return self._groups[group_name] - - def _get_opt_info(self, opt_name, group=None): - """Return the (opt, override, default) dict for an opt. - - :param opt_name: an opt name/dest - :param group: an optional group name or OptGroup object - :raises: NoSuchOptError, NoSuchGroupError - """ - if group is None: - opts = self._opts - else: - group = self._get_group(group) - opts = group._opts - - if not opt_name in opts: - raise NoSuchOptError(opt_name, group) - - return opts[opt_name] - - def _parse_config_files(self): - """Parse the config files from --config-file and --config-dir. - - :raises: ConfigFilesNotFoundError, ConfigFileParseError - """ - config_files = list(self.config_file) - - if self.config_dir: - config_dir_glob = os.path.join(self.config_dir, '*.conf') - config_files += sorted(glob.glob(config_dir_glob)) - - config_files = [_fixpath(p) for p in config_files] - - self._cparser = MultiConfigParser() - - try: - read_ok = self._cparser.read(config_files) - except iniparser.ParseError as pe: - raise ConfigFileParseError(pe.filename, str(pe)) - - if read_ok != config_files: - not_read_ok = filter(lambda f: f not in read_ok, config_files) - raise ConfigFilesNotFoundError(not_read_ok) - - def _check_required_opts(self): - """Check that all opts marked as required have values specified. - - :raises: RequiredOptError - """ - for info, group in self._all_opt_infos(): - opt = info['opt'] - - if opt.required: - if ('default' in info or 'override' in info): - continue - - if self._get(opt.dest, group) is None: - raise RequiredOptError(opt.name, group) - - def _parse_cli_opts(self, args): - """Parse command line options. - - Initializes the command line option parser and parses the supplied - command line arguments. - - :param args: the command line arguments - :returns: a dict of parsed option values - :raises: SystemExit, DuplicateOptError - - """ - self._args = args - - for opt, group in self._all_opts(): - opt._add_to_cli(self._oparser, group) - - values, leftovers = self._oparser.parse_args(args) - - return vars(values), leftovers - - class GroupAttr(collections.Mapping): - - """ - A helper class representing the option values of a group as a mapping - and attributes. - """ - - def __init__(self, conf, group): - """Construct a GroupAttr object. - - :param conf: a ConfigOpts object - :param group: an OptGroup object - """ - self.conf = conf - self.group = group - - def __getattr__(self, name): - """Look up an option value and perform template substitution.""" - return self.conf._get(name, self.group) - - def __getitem__(self, key): - """Look up an option value and perform string substitution.""" - return self.__getattr__(key) - - def __contains__(self, key): - """Return True if key is the name of a registered opt or group.""" - return key in self.group._opts - - def __iter__(self): - """Iterate over all registered opt and group names.""" - for key in self.group._opts.keys(): - yield key - - def __len__(self): - """Return the number of options and option groups.""" - return len(self.group._opts) - - class StrSubWrapper(object): - - """ - A helper class exposing opt values as a dict for string substitution. - """ - - def __init__(self, conf): - """Construct a StrSubWrapper object. - - :param conf: a ConfigOpts object - """ - self.conf = conf - - def __getitem__(self, key): - """Look up an opt value from the ConfigOpts object. - - :param key: an opt name - :returns: an opt value - :raises: TemplateSubstitutionError if attribute is a group - """ - value = getattr(self.conf, key) - if isinstance(value, self.conf.GroupAttr): - raise TemplateSubstitutionError( - 'substituting group %s not supported' % key) - return value - - -class CommonConfigOpts(ConfigOpts): - - DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" - DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" - - common_cli_opts = [ - BoolOpt('debug', - short='d', - default=False, - help='Print debugging output'), - BoolOpt('verbose', - short='v', - default=False, - help='Print more verbose output'), - ] - - logging_cli_opts = [ - StrOpt('log-config', - metavar='PATH', - help='If this option is specified, the logging configuration ' - 'file specified is used and overrides any other logging ' - 'options specified. Please see the Python logging module ' - 'documentation for details on logging configuration ' - 'files.'), - StrOpt('log-format', - default=DEFAULT_LOG_FORMAT, - metavar='FORMAT', - help='A logging.Formatter log message format string which may ' - 'use any of the available logging.LogRecord attributes. ' - 'Default: %default'), - StrOpt('log-date-format', - default=DEFAULT_LOG_DATE_FORMAT, - metavar='DATE_FORMAT', - help='Format string for %(asctime)s in log records. ' - 'Default: %default'), - StrOpt('log-file', - metavar='PATH', - help='(Optional) Name of log file to output to. ' - 'If not set, logging will go to stdout.'), - StrOpt('log-dir', - help='(Optional) The directory to keep log files in ' - '(will be prepended to --logfile)'), - BoolOpt('use-syslog', - default=False, - help='Use syslog for logging.'), - StrOpt('syslog-log-facility', - default='LOG_USER', - help='syslog facility to receive log lines') - ] - - def __init__(self): - super(CommonConfigOpts, self).__init__() - self.register_cli_opts(self.common_cli_opts) - self.register_cli_opts(self.logging_cli_opts) - - -CONF = CommonConfigOpts() diff --git a/keystoneclient/openstack/common/iniparser.py b/keystoneclient/openstack/common/iniparser.py deleted file mode 100644 index 241284449..000000000 --- a/keystoneclient/openstack/common/iniparser.py +++ /dev/null @@ -1,130 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -class ParseError(Exception): - def __init__(self, message, lineno, line): - self.msg = message - self.line = line - self.lineno = lineno - - def __str__(self): - return 'at line %d, %s: %r' % (self.lineno, self.msg, self.line) - - -class BaseParser(object): - lineno = 0 - parse_exc = ParseError - - def _assignment(self, key, value): - self.assignment(key, value) - return None, [] - - def _get_section(self, line): - if line[-1] != ']': - return self.error_no_section_end_bracket(line) - if len(line) <= 2: - return self.error_no_section_name(line) - - return line[1:-1] - - def _split_key_value(self, line): - colon = line.find(':') - equal = line.find('=') - if colon < 0 and equal < 0: - return self.error_invalid_assignment(line) - - if colon < 0 or (equal >= 0 and equal < colon): - key, value = line[:equal], line[equal + 1:] - else: - key, value = line[:colon], line[colon + 1:] - - value = value.strip() - if ((value and value[0] == value[-1]) and - (value[0] == "\"" or value[0] == "'")): - value = value[1:-1] - return key.strip(), [value] - - def parse(self, lineiter): - key = None - value = [] - - for line in lineiter: - self.lineno += 1 - - line = line.rstrip() - if not line: - # Blank line, ends multi-line values - if key: - key, value = self._assignment(key, value) - continue - elif line[0] in (' ', '\t'): - # Continuation of previous assignment - if key is None: - self.error_unexpected_continuation(line) - else: - value.append(line.lstrip()) - continue - - if key: - # Flush previous assignment, if any - key, value = self._assignment(key, value) - - if line[0] == '[': - # Section start - section = self._get_section(line) - if section: - self.new_section(section) - elif line[0] in '#;': - self.comment(line[1:].lstrip()) - else: - key, value = self._split_key_value(line) - if not key: - return self.error_empty_key(line) - - if key: - # Flush previous assignment, if any - self._assignment(key, value) - - def assignment(self, key, value): - """Called when a full assignment is parsed""" - raise NotImplementedError() - - def new_section(self, section): - """Called when a new section is started""" - raise NotImplementedError() - - def comment(self, comment): - """Called when a comment is parsed""" - pass - - def error_invalid_assignment(self, line): - raise self.parse_exc("No ':' or '=' found in assignment", - self.lineno, line) - - def error_empty_key(self, line): - raise self.parse_exc('Key cannot be empty', self.lineno, line) - - def error_unexpected_continuation(self, line): - raise self.parse_exc('Unexpected continuation line', - self.lineno, line) - - def error_no_section_end_bracket(self, line): - raise self.parse_exc('Invalid section (must end with ])', - self.lineno, line) - - def error_no_section_name(self, line): - raise self.parse_exc('Empty section name', self.lineno, line) diff --git a/keystoneclient/openstack/common/jsonutils.py b/keystoneclient/openstack/common/jsonutils.py deleted file mode 100644 index d06ba5951..000000000 --- a/keystoneclient/openstack/common/jsonutils.py +++ /dev/null @@ -1,148 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Justin Santa Barbara -# 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. - -''' -JSON related utilities. - -This module provides a few things: - - 1) A handy function for getting an object down to something that can be - JSON serialized. See to_primitive(). - - 2) Wrappers around loads() and dumps(). The dumps() wrapper will - automatically use to_primitive() for you if needed. - - 3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson - is available. -''' - - -import datetime -import inspect -import itertools -import json -import xmlrpclib - -from keystoneclient.openstack.common import timeutils - - -def to_primitive(value, convert_instances=False, level=0): - """Convert a complex object into primitives. - - Handy for JSON serialization. We can optionally handle instances, - but since this is a recursive function, we could have cyclical - data structures. - - To handle cyclical data structures we could track the actual objects - visited in a set, but not all objects are hashable. Instead we just - track the depth of the object inspections and don't go too deep. - - Therefore, convert_instances=True is lossy ... be aware. - - """ - nasty = [inspect.ismodule, inspect.isclass, inspect.ismethod, - inspect.isfunction, inspect.isgeneratorfunction, - inspect.isgenerator, inspect.istraceback, inspect.isframe, - inspect.iscode, inspect.isbuiltin, inspect.isroutine, - inspect.isabstract] - for test in nasty: - if test(value): - return unicode(value) - - # value of itertools.count doesn't get caught by inspects - # above and results in infinite loop when list(value) is called. - if type(value) == itertools.count: - return unicode(value) - - # FIXME(vish): Workaround for LP bug 852095. Without this workaround, - # tests that raise an exception in a mocked method that - # has a @wrap_exception with a notifier will fail. If - # we up the dependency to 0.5.4 (when it is released) we - # can remove this workaround. - if getattr(value, '__module__', None) == 'mox': - return 'mock' - - if level > 3: - return '?' - - # The try block may not be necessary after the class check above, - # but just in case ... - try: - # It's not clear why xmlrpclib created their own DateTime type, but - # for our purposes, make it a datetime type which is explicitly - # handled - if isinstance(value, xmlrpclib.DateTime): - value = datetime.datetime(*tuple(value.timetuple())[:6]) - - if isinstance(value, (list, tuple)): - o = [] - for v in value: - o.append(to_primitive(v, convert_instances=convert_instances, - level=level)) - return o - elif isinstance(value, dict): - o = {} - for k, v in value.iteritems(): - o[k] = to_primitive(v, convert_instances=convert_instances, - level=level) - return o - elif isinstance(value, datetime.datetime): - return timeutils.strtime(value) - elif hasattr(value, 'iteritems'): - return to_primitive(dict(value.iteritems()), - convert_instances=convert_instances, - level=level + 1) - elif hasattr(value, '__iter__'): - return to_primitive(list(value), - convert_instances=convert_instances, - level=level) - elif convert_instances and hasattr(value, '__dict__'): - # Likely an instance of something. Watch for cycles. - # Ignore class member vars. - return to_primitive(value.__dict__, - convert_instances=convert_instances, - level=level + 1) - else: - return value - except TypeError, e: - # Class objects are tricky since they may define something like - # __iter__ defined but it isn't callable as list(). - return unicode(value) - - -def dumps(value, default=to_primitive, **kwargs): - return json.dumps(value, default=default, **kwargs) - - -def loads(s): - return json.loads(s) - - -def load(s): - return json.load(s) - - -try: - import anyjson -except ImportError: - pass -else: - anyjson._modules.append((__name__, 'dumps', TypeError, - 'loads', ValueError, 'load')) - anyjson.force_implementation(__name__) diff --git a/keystoneclient/openstack/common/setup.py b/keystoneclient/openstack/common/setup.py deleted file mode 100644 index e6f72f034..000000000 --- a/keystoneclient/openstack/common/setup.py +++ /dev/null @@ -1,366 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Utilities with minimum-depends for use in setup.py -""" - -import datetime -import os -import re -import subprocess -import sys - -from setuptools.command import sdist - - -def parse_mailmap(mailmap='.mailmap'): - mapping = {} - if os.path.exists(mailmap): - with open(mailmap, 'r') as fp: - for l in fp: - l = l.strip() - if not l.startswith('#') and ' ' in l: - canonical_email, alias = [x for x in l.split(' ') - if x.startswith('<')] - mapping[alias] = canonical_email - return mapping - - -def canonicalize_emails(changelog, mapping): - """Takes in a string and an email alias mapping and replaces all - instances of the aliases in the string with their real email. - """ - for alias, email in mapping.iteritems(): - changelog = changelog.replace(alias, email) - return changelog - - -# Get requirements from the first file that exists -def get_reqs_from_files(requirements_files): - for requirements_file in requirements_files: - if os.path.exists(requirements_file): - with open(requirements_file, 'r') as fil: - return fil.read().split('\n') - return [] - - -def parse_requirements(requirements_files=['requirements.txt', - 'tools/pip-requires']): - requirements = [] - for line in get_reqs_from_files(requirements_files): - # For the requirements list, we need to inject only the portion - # after egg= so that distutils knows the package it's looking for - # such as: - # -e git://github.com/openstack/nova/master#egg=nova - if re.match(r'\s*-e\s+', line): - requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', - line)) - # such as: - # http://github.com/openstack/nova/zipball/master#egg=nova - elif re.match(r'\s*https?:', line): - requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', - line)) - # -f lines are for index locations, and don't get used here - elif re.match(r'\s*-f\s+', line): - pass - # argparse is part of the standard library starting with 2.7 - # adding it to the requirements list screws distro installs - elif line == 'argparse' and sys.version_info >= (2, 7): - pass - else: - requirements.append(line) - - return requirements - - -def parse_dependency_links(requirements_files=['requirements.txt', - 'tools/pip-requires']): - dependency_links = [] - # dependency_links inject alternate locations to find packages listed - # in requirements - for line in get_reqs_from_files(requirements_files): - # skip comments and blank lines - if re.match(r'(\s*#)|(\s*$)', line): - continue - # lines with -e or -f need the whole line, minus the flag - if re.match(r'\s*-[ef]\s+', line): - dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line)) - # lines that are only urls can go in unmolested - elif re.match(r'\s*https?:', line): - dependency_links.append(line) - return dependency_links - - -def write_requirements(): - venv = os.environ.get('VIRTUAL_ENV', None) - if venv is not None: - with open("requirements.txt", "w") as req_file: - output = subprocess.Popen(["pip", "-E", venv, "freeze", "-l"], - stdout=subprocess.PIPE) - requirements = output.communicate()[0].strip() - req_file.write(requirements) - - -def _run_shell_command(cmd): - if os.name == 'nt': - output = subprocess.Popen(["cmd.exe", "/C", cmd], - stdout=subprocess.PIPE) - else: - output = subprocess.Popen(["/bin/sh", "-c", cmd], - stdout=subprocess.PIPE) - out = output.communicate() - if len(out) == 0: - return None - if len(out[0].strip()) == 0: - return None - return out[0].strip() - - -def _get_git_next_version_suffix(branch_name): - datestamp = datetime.datetime.now().strftime('%Y%m%d') - if branch_name == 'milestone-proposed': - revno_prefix = "r" - else: - revno_prefix = "" - _run_shell_command("git fetch origin +refs/meta/*:refs/remotes/meta/*") - milestone_cmd = "git show meta/openstack/release:%s" % branch_name - milestonever = _run_shell_command(milestone_cmd) - if milestonever: - first_half = "%s~%s" % (milestonever, datestamp) - else: - first_half = datestamp - - post_version = _get_git_post_version() - # post version should look like: - # 0.1.1.4.gcc9e28a - # where the bit after the last . is the short sha, and the bit between - # the last and second to last is the revno count - (revno, sha) = post_version.split(".")[-2:] - second_half = "%s%s.%s" % (revno_prefix, revno, sha) - return ".".join((first_half, second_half)) - - -def _get_git_current_tag(): - return _run_shell_command("git tag --contains HEAD") - - -def _get_git_tag_info(): - return _run_shell_command("git describe --tags") - - -def _get_git_post_version(): - current_tag = _get_git_current_tag() - if current_tag is not None: - return current_tag - else: - tag_info = _get_git_tag_info() - if tag_info is None: - base_version = "0.0" - cmd = "git --no-pager log --oneline" - out = _run_shell_command(cmd) - revno = len(out.split("\n")) - sha = _run_shell_command("git describe --always") - else: - tag_infos = tag_info.split("-") - base_version = "-".join(tag_infos[:-2]) - (revno, sha) = tag_infos[-2:] - return "%s.%s.%s" % (base_version, revno, sha) - - -def write_git_changelog(): - """Write a changelog based on the git changelog.""" - new_changelog = 'ChangeLog' - if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'): - if os.path.isdir('.git'): - git_log_cmd = 'git log --stat' - changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() - with open(new_changelog, "w") as changelog_file: - changelog_file.write(canonicalize_emails(changelog, mailmap)) - else: - open(new_changelog, 'w').close() - - -def generate_authors(): - """Create AUTHORS file using git commits.""" - jenkins_email = 'jenkins@review.(openstack|stackforge).org' - old_authors = 'AUTHORS.in' - new_authors = 'AUTHORS' - if not os.getenv('SKIP_GENERATE_AUTHORS'): - if os.path.isdir('.git'): - # don't include jenkins email address in AUTHORS file - git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | " - "egrep -v '" + jenkins_email + "'") - changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() - with open(new_authors, 'w') as new_authors_fh: - new_authors_fh.write(canonicalize_emails(changelog, mailmap)) - if os.path.exists(old_authors): - with open(old_authors, "r") as old_authors_fh: - new_authors_fh.write('\n' + old_authors_fh.read()) - else: - open(new_authors, 'w').close() - - -_rst_template = """%(heading)s -%(underline)s - -.. automodule:: %(module)s - :members: - :undoc-members: - :show-inheritance: -""" - - -def read_versioninfo(project): - """Read the versioninfo file. If it doesn't exist, we're in a github - zipball, and there's really no way to know what version we really - are, but that should be ok, because the utility of that should be - just about nil if this code path is in use in the first place.""" - versioninfo_path = os.path.join(project, 'versioninfo') - if os.path.exists(versioninfo_path): - with open(versioninfo_path, 'r') as vinfo: - version = vinfo.read().strip() - else: - version = "0.0.0" - return version - - -def write_versioninfo(project, version): - """Write a simple file containing the version of the package.""" - with open(os.path.join(project, 'versioninfo'), 'w') as fil: - fil.write("%s\n" % version) - - -def get_cmdclass(): - """Return dict of commands to run from setup.py.""" - - cmdclass = dict() - - def _find_modules(arg, dirname, files): - for filename in files: - if filename.endswith('.py') and filename != '__init__.py': - arg["%s.%s" % (dirname.replace('/', '.'), - filename[:-3])] = True - - class LocalSDist(sdist.sdist): - """Builds the ChangeLog and Authors files from VC first.""" - - def run(self): - write_git_changelog() - generate_authors() - # sdist.sdist is an old style class, can't use super() - sdist.sdist.run(self) - - cmdclass['sdist'] = LocalSDist - - # If Sphinx is installed on the box running setup.py, - # enable setup.py to build the documentation, otherwise, - # just ignore it - try: - from sphinx.setup_command import BuildDoc - - class LocalBuildDoc(BuildDoc): - def generate_autoindex(self): - print "**Autodocumenting from %s" % os.path.abspath(os.curdir) - modules = {} - option_dict = self.distribution.get_option_dict('build_sphinx') - source_dir = os.path.join(option_dict['source_dir'][1], 'api') - if not os.path.exists(source_dir): - os.makedirs(source_dir) - for pkg in self.distribution.packages: - if '.' not in pkg: - os.path.walk(pkg, _find_modules, modules) - module_list = modules.keys() - module_list.sort() - autoindex_filename = os.path.join(source_dir, 'autoindex.rst') - with open(autoindex_filename, 'w') as autoindex: - autoindex.write(""".. toctree:: - :maxdepth: 1 - -""") - for module in module_list: - output_filename = os.path.join(source_dir, - "%s.rst" % module) - heading = "The :mod:`%s` Module" % module - underline = "=" * len(heading) - values = dict(module=module, heading=heading, - underline=underline) - - print "Generating %s" % output_filename - with open(output_filename, 'w') as output_file: - output_file.write(_rst_template % values) - autoindex.write(" %s.rst\n" % module) - - def run(self): - if not os.getenv('SPHINX_DEBUG'): - self.generate_autoindex() - - for builder in ['html', 'man']: - self.builder = builder - self.finalize_options() - self.project = self.distribution.get_name() - self.version = self.distribution.get_version() - self.release = self.distribution.get_version() - BuildDoc.run(self) - cmdclass['build_sphinx'] = LocalBuildDoc - except ImportError: - pass - - return cmdclass - - -def get_git_branchname(): - for branch in _run_shell_command("git branch --color=never").split("\n"): - if branch.startswith('*'): - _branch_name = branch.split()[1].strip() - if _branch_name == "(no": - _branch_name = "no-branch" - return _branch_name - - -def get_pre_version(projectname, base_version): - """Return a version which is leading up to a version that will - be released in the future.""" - if os.path.isdir('.git'): - current_tag = _get_git_current_tag() - if current_tag is not None: - version = current_tag - else: - branch_name = os.getenv('BRANCHNAME', - os.getenv('GERRIT_REFNAME', - get_git_branchname())) - version_suffix = _get_git_next_version_suffix(branch_name) - version = "%s~%s" % (base_version, version_suffix) - write_versioninfo(projectname, version) - return version - else: - version = read_versioninfo(projectname) - return version - - -def get_post_version(projectname): - """Return a version which is equal to the tag that's on the current - revision if there is one, or tag plus number of additional revisions - if the current revision has no tag.""" - - if os.path.isdir('.git'): - version = _get_git_post_version() - write_versioninfo(projectname, version) - return version - return read_versioninfo(projectname) diff --git a/keystoneclient/openstack/common/timeutils.py b/keystoneclient/openstack/common/timeutils.py deleted file mode 100644 index 86004391d..000000000 --- a/keystoneclient/openstack/common/timeutils.py +++ /dev/null @@ -1,137 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Time related utilities and helper functions. -""" - -import calendar -import datetime - -import iso8601 - - -TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" -PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" - - -def isotime(at=None): - """Stringify time in ISO 8601 format""" - if not at: - at = utcnow() - str = at.strftime(TIME_FORMAT) - tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' - str += ('Z' if tz == 'UTC' else tz) - return str - - -def parse_isotime(timestr): - """Parse time from ISO 8601 format""" - try: - return iso8601.parse_date(timestr) - except iso8601.ParseError as e: - raise ValueError(e.message) - except TypeError as e: - raise ValueError(e.message) - - -def strtime(at=None, fmt=PERFECT_TIME_FORMAT): - """Returns formatted utcnow.""" - if not at: - at = utcnow() - return at.strftime(fmt) - - -def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): - """Turn a formatted time back into a datetime.""" - return datetime.datetime.strptime(timestr, fmt) - - -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 is_older_than(before, seconds): - """Return True if before is older than seconds.""" - return utcnow() - before > datetime.timedelta(seconds=seconds) - - -def is_newer_than(after, seconds): - """Return True if after is newer than seconds.""" - return after - utcnow() > datetime.timedelta(seconds=seconds) - - -def utcnow_ts(): - """Timestamp version of our utcnow function.""" - return calendar.timegm(utcnow().timetuple()) - - -def utcnow(): - """Overridable version of utils.utcnow.""" - if utcnow.override_time: - return utcnow.override_time - return datetime.datetime.utcnow() - - -utcnow.override_time = None - - -def set_time_override(override_time=datetime.datetime.utcnow()): - """Override utils.utcnow to return a constant time.""" - utcnow.override_time = override_time - - -def advance_time_delta(timedelta): - """Advance overridden time using a datetime.timedelta.""" - assert(not utcnow.override_time is None) - utcnow.override_time += timedelta - - -def advance_time_seconds(seconds): - """Advance overridden time by seconds.""" - advance_time_delta(datetime.timedelta(0, seconds)) - - -def clear_time_override(): - """Remove the overridden time.""" - utcnow.override_time = None - - -def marshall_now(now=None): - """Make an rpc-safe datetime with microseconds. - - Note: tzinfo is stripped, but not required for relative times.""" - if not now: - now = utcnow() - return dict(day=now.day, month=now.month, year=now.year, hour=now.hour, - minute=now.minute, second=now.second, - microsecond=now.microsecond) - - -def unmarshall_time(tyme): - """Unmarshall a datetime dict.""" - return datetime.datetime(day=tyme['day'], - month=tyme['month'], - year=tyme['year'], - hour=tyme['hour'], - minute=tyme['minute'], - second=tyme['second'], - microsecond=tyme['microsecond']) diff --git a/keystoneclient/service_catalog.py b/keystoneclient/service_catalog.py index 50587c270..afbefc0ae 100644 --- a/keystoneclient/service_catalog.py +++ b/keystoneclient/service_catalog.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011, Piston Cloud Computing, Inc. # Copyright 2011 Nebula, Inc. # @@ -16,16 +16,75 @@ # See the License for the specific language governing permissions and # limitations under the License. +import abc +import warnings from keystoneclient import exceptions +from keystoneclient.i18n import _ -class ServiceCatalog(object): - """Helper methods for dealing with a Keystone Service Catalog.""" +class ServiceCatalog(object, metaclass=abc.ABCMeta): + """Helper methods for dealing with a Keystone Service Catalog. - def __init__(self, resource_dict): - self.catalog = resource_dict + .. warning:: + + Setting region_name is deprecated in favor of passing the region name + as a parameter to calls made to the service catalog as of the 1.7.0 + release and may be removed in the 2.0.0 release. + + """ + + @classmethod + def factory(cls, resource_dict, token=None, region_name=None): + """Create ServiceCatalog object given an auth token. + + .. warning:: + + Setting region_name is deprecated in favor of passing the region + name as a parameter to calls made to the service catalog as of the + 1.7.0 release and may be removed in the 2.0.0 release. + + """ + if ServiceCatalogV3.is_valid(resource_dict): + return ServiceCatalogV3(token, resource_dict, region_name) + elif ServiceCatalogV2.is_valid(resource_dict): + return ServiceCatalogV2(resource_dict, region_name) + else: + raise NotImplementedError(_('Unrecognized auth response')) + + def __init__(self, region_name=None): + if region_name: + warnings.warn( + 'Setting region_name on the service catalog is deprecated in ' + 'favor of passing the region name as a parameter to calls ' + 'made to the service catalog as of the 1.7.0 release and may ' + 'be removed in the 2.0.0 release.', + DeprecationWarning) + + self._region_name = region_name + + @property + def region_name(self): + """Region name. + + .. warning:: + + region_name is deprecated in favor of passing the region name as a + parameter to calls made to the service catalog as of the 1.7.0 + release and may be removed in the 2.0.0 release. + + """ + warnings.warn( + 'region_name is deprecated in favor of passing the region name as ' + 'a parameter to calls made to the service catalog as of the 1.7.0 ' + 'release and may be removed in the 2.0.0 release.', + DeprecationWarning) + return self._region_name + + def _get_endpoint_region(self, endpoint): + return endpoint.get('region_id') or endpoint.get('region') + @abc.abstractmethod def get_token(self): """Fetch token details from service catalog. @@ -35,59 +94,331 @@ def get_token(self): - `expires`: Token's expiration - `user_id`: Authenticated user's ID - `tenant_id`: Authorized project's ID + - `domain_id`: Authorized domain's ID """ - token = {'id': self.catalog['token']['id'], - 'expires': self.catalog['token']['expires']} + raise NotImplementedError() # pragma: no cover + + @abc.abstractmethod + def _is_endpoint_type_match(self, endpoint, endpoint_type): + """Helper function to normalize endpoint matching across v2 and v3. + + :returns: True if the provided endpoint matches the required + endpoint_type otherwise False. + """ + pass # pragma: no cover + + @abc.abstractmethod + def _normalize_endpoint_type(self, endpoint_type): + """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' endpoint_type and + v3 must be able to handle a 'publicURL' endpoint_type. + + :returns: the endpoint string in the format appropriate for this + service catalog. + """ + pass # pragma: no cover + + def get_endpoints(self, service_type=None, endpoint_type=None, + region_name=None, service_name=None): + """Fetch and filter endpoints for the specified service(s). + + Returns endpoints for the specified service (or all) containing + the specified type (or all) and region (or all) and service name. + + If there is no name in the service catalog the service_name check will + be skipped. This allows compatibility with services that existed + before the name was available in the catalog. + """ + endpoint_type = self._normalize_endpoint_type(endpoint_type) + region_name = region_name or self._region_name + + sc = {} + + for service in (self.get_data() or []): + try: + st = service['type'] + except KeyError: + continue + + if service_type and service_type != st: + continue + + # NOTE(jamielennox): service_name is different. It is not available + # in API < v3.3. If it is in the catalog then we enforce it, if it + # is not then we don't because the name could be correct we just + # don't have that information to check against. + if service_name: + try: + sn = service['name'] + except KeyError: # nosec(cjschaef) + # assume that we're in v3.0-v3.2 and don't have the name in + # the catalog. Skip the check. + pass + else: + if service_name != sn: + continue + + endpoints = sc.setdefault(st, []) + + for endpoint in service.get('endpoints', []): + if (endpoint_type and not + self._is_endpoint_type_match(endpoint, endpoint_type)): + continue + if (region_name and + region_name != self._get_endpoint_region(endpoint)): + continue + endpoints.append(endpoint) + + return sc + + def _get_service_endpoints(self, attr, filter_value, service_type, + endpoint_type, region_name, service_name): + """Fetch the endpoints of a particular service_type. + + Endpoints returned are also filtered based on the attr and + filter_value provided. + """ + sc_endpoints = self.get_endpoints(service_type=service_type, + endpoint_type=endpoint_type, + region_name=region_name, + service_name=service_name) + try: - token['user_id'] = self.catalog['user']['id'] - token['tenant_id'] = self.catalog['token']['tenant']['id'] - except Exception: - # just leave the tenant and user out if it doesn't exist - pass - return token + endpoints = sc_endpoints[service_type] + except KeyError: + return + + if attr and not filter_value: + warnings.warn( + 'Providing attr without filter_value to get_urls() is ' + 'deprecated as of the 1.7.0 release and may be removed in the ' + '2.0.0 release. Either both should be provided or neither ' + 'should be provided.') + + if filter_value: + return [endpoint for endpoint in endpoints + if endpoint.get(attr) == filter_value] + + return endpoints + + @abc.abstractmethod + def get_urls(self, attr=None, filter_value=None, + service_type='identity', endpoint_type='publicURL', + region_name=None, service_name=None): + """Fetch endpoint urls from the service catalog. + + Fetch the endpoints from the service catalog for a particular + endpoint attribute. If no attribute is given, return the first + endpoint of the specified type. + + :param string attr: Endpoint attribute name. + :param string filter_value: Endpoint attribute value. + :param string service_type: Service type of the endpoint. + :param string endpoint_type: Type of endpoint. + Possible values: public or publicURL, + internal or internalURL, admin or + adminURL + :param string region_name: Region of the endpoint. + :param string service_name: The assigned name of the service. + + :returns: tuple of urls or None (if no match found) + """ + raise NotImplementedError() # pragma: no cover def url_for(self, attr=None, filter_value=None, - service_type='identity', endpoint_type='publicURL'): + service_type='identity', endpoint_type='publicURL', + region_name=None, service_name=None): """Fetch an endpoint from the service catalog. Fetch the specified endpoint from the service catalog for a particular endpoint attribute. If no attribute is given, return the first endpoint of the specified type. - Valid endpoint types: `publicURL`, `internalURL`, `adminURL` + Valid endpoint types: `public` or `publicURL`, + `internal` or `internalURL`, + `admin` or 'adminURL` + + :param string attr: Endpoint attribute name. + :param string filter_value: Endpoint attribute value. + :param string service_type: Service type of the endpoint. + :param string endpoint_type: Type of endpoint. + :param string region_name: Region of the endpoint. + :param string service_name: The assigned name of the service. - See tests for a sample service catalog. """ - catalog = self.catalog.get('serviceCatalog', []) + if not self.get_data(): + raise exceptions.EmptyCatalog(_('The service catalog is empty.')) - if not catalog: - raise exceptions.EmptyCatalog('The service catalog is empty.') + urls = self.get_urls(attr=attr, + filter_value=filter_value, + service_type=service_type, + endpoint_type=endpoint_type, + region_name=region_name, + service_name=service_name) - for service in catalog: - if service['type'] != service_type: - continue + try: + return urls[0] + except Exception: + if service_name and region_name: + msg = (_('%(endpoint_type)s endpoint for %(service_type)s ' + 'service named %(service_name)s in %(region_name)s ' + 'region not found') % + {'endpoint_type': endpoint_type, + 'service_type': service_type, + 'service_name': service_name, + 'region_name': region_name}) + elif service_name: + msg = (_('%(endpoint_type)s endpoint for %(service_type)s ' + 'service named %(service_name)s not found') % + {'endpoint_type': endpoint_type, + 'service_type': service_type, + 'service_name': service_name}) + elif region_name: + msg = (_('%(endpoint_type)s endpoint for %(service_type)s ' + 'service in %(region_name)s region not found') % + {'endpoint_type': endpoint_type, + 'service_type': service_type, + 'region_name': region_name}) + else: + msg = (_('%(endpoint_type)s endpoint for %(service_type)s ' + 'service not found') % + {'endpoint_type': endpoint_type, + 'service_type': service_type}) - endpoints = service['endpoints'] - for endpoint in endpoints: - if not filter_value or endpoint.get(attr) == filter_value: - return endpoint[endpoint_type] + raise exceptions.EndpointNotFound(msg) - raise exceptions.EndpointNotFound('Endpoint not found.') + @abc.abstractmethod + def get_data(self): + """Get the raw catalog structure. - def get_endpoints(self, service_type=None, endpoint_type=None): - """Fetch and filter endpoints for the specified service(s). + Get the version dependent catalog structure as it is presented within + the resource. - Returns endpoints for the specified service (or all) and - that contain the specified type (or all). + :returns: list containing raw catalog data entries or None """ - sc = {} - for service in self.catalog.get('serviceCatalog', []): - if service_type and service_type != service['type']: - continue - sc[service['type']] = [] - for endpoint in service['endpoints']: - if endpoint_type and endpoint_type not in endpoint.keys(): - continue - sc[service['type']].append(endpoint) - return sc + raise NotImplementedError() # pragma: no cover + + +class ServiceCatalogV2(ServiceCatalog): + """An object for encapsulating the v2 service catalog. + + The object is created using raw v2 auth token from Keystone. + """ + + def __init__(self, resource_dict, region_name=None): + self.catalog = resource_dict + super(ServiceCatalogV2, self).__init__(region_name=region_name) + + @classmethod + def is_valid(cls, resource_dict): + # This class is also used for reading token info of an unscoped token. + # Unscoped token does not have 'serviceCatalog' in V2, checking this + # will not work. Use 'token' attribute instead. + return 'token' in resource_dict + + def _normalize_endpoint_type(self, endpoint_type): + if endpoint_type and 'URL' not in endpoint_type: + endpoint_type += 'URL' + + return endpoint_type + + def _is_endpoint_type_match(self, endpoint, endpoint_type): + return endpoint_type in endpoint + + def get_data(self): + return self.catalog.get('serviceCatalog') + + def get_token(self): + token = {'id': self.catalog['token']['id'], + 'expires': self.catalog['token']['expires']} + try: + token['user_id'] = self.catalog['user']['id'] + token['tenant_id'] = self.catalog['token']['tenant']['id'] + except KeyError: # nosec(cjschaef) + # just leave the tenant and user out if it doesn't exist + pass + return token + + def get_urls(self, attr=None, filter_value=None, + service_type='identity', endpoint_type='publicURL', + region_name=None, service_name=None): + endpoint_type = self._normalize_endpoint_type(endpoint_type) + endpoints = self._get_service_endpoints(attr=attr, + filter_value=filter_value, + service_type=service_type, + endpoint_type=endpoint_type, + region_name=region_name, + service_name=service_name) + + if endpoints: + return tuple([endpoint[endpoint_type] for endpoint in endpoints]) + else: + return None + + +class ServiceCatalogV3(ServiceCatalog): + """An object for encapsulating the v3 service catalog. + + The object is created using raw v3 auth token from Keystone. + """ + + def __init__(self, token, resource_dict, region_name=None): + super(ServiceCatalogV3, self).__init__(region_name=region_name) + self._auth_token = token + self.catalog = resource_dict + + @classmethod + def is_valid(cls, resource_dict): + # This class is also used for reading token info of an unscoped token. + # Unscoped token does not have 'catalog', checking this + # will not work. Use 'methods' attribute instead. + return 'methods' in resource_dict + + def _normalize_endpoint_type(self, endpoint_type): + if endpoint_type: + endpoint_type = endpoint_type.rstrip('URL') + + return endpoint_type + + def _is_endpoint_type_match(self, endpoint, endpoint_type): + try: + return endpoint_type == endpoint['interface'] + except KeyError: + return False + + def get_data(self): + return self.catalog.get('catalog') + + def get_token(self): + token = {'id': self._auth_token, + 'expires': self.catalog['expires_at']} + try: + token['user_id'] = self.catalog['user']['id'] + domain = self.catalog.get('domain') + if domain: + token['domain_id'] = domain['id'] + project = self.catalog.get('project') + if project: + token['tenant_id'] = project['id'] + except KeyError: # nosec(cjschaef) + # just leave the domain, project and user out if it doesn't exist + pass + return token + + def get_urls(self, attr=None, filter_value=None, + service_type='identity', endpoint_type='public', + region_name=None, service_name=None): + endpoints = self._get_service_endpoints(attr=attr, + filter_value=filter_value, + service_type=service_type, + endpoint_type=endpoint_type, + region_name=region_name, + service_name=service_name) + + if endpoints: + return tuple([endpoint['url'] for endpoint in endpoints]) + else: + return None diff --git a/keystoneclient/session.py b/keystoneclient/session.py new file mode 100644 index 000000000..4944d45f6 --- /dev/null +++ b/keystoneclient/session.py @@ -0,0 +1,1017 @@ +# 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 functools +import hashlib +import logging +import os +import socket +import time +import urllib.parse +import warnings + +from debtcollector import removals +from oslo_config import cfg +from oslo_serialization import jsonutils +from oslo_utils import encodeutils +from oslo_utils import importutils +from oslo_utils import strutils +import requests + +from keystoneclient import exceptions +from keystoneclient.i18n import _ + +osprofiler_web = importutils.try_import("osprofiler.web") + +USER_AGENT = 'python-keystoneclient' + +# NOTE(jamielennox): Clients will likely want to print more than json. Please +# propose a patch if you have a content type you think is reasonable to print +# here and we'll add it to the list as required. +_LOG_CONTENT_TYPES = set(['application/json']) + +_logger = logging.getLogger(__name__) + + +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 + + +def request(url, method='GET', **kwargs): + return Session().request(url, method=method, **kwargs) + + +def _remove_service_catalog(body): + try: + data = jsonutils.loads(body) + + # V3 token + if 'token' in data and 'catalog' in data['token']: + data['token']['catalog'] = '' + return jsonutils.dumps(data) + + # V2 token + if 'serviceCatalog' in data['access']: + data['access']['serviceCatalog'] = '' + return jsonutils.dumps(data) + + except Exception: # nosec(cjschaef): multiple exceptions can be raised + # Don't fail trying to clean up the request body. + pass + return body + + +class Session(object): + """Maintains client communication state and common functionality. + + As much as possible the parameters to this class reflect and are passed + directly to the requests library. + + :param auth: An authentication plugin to authenticate the session with. + (optional, defaults to None) + :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` + :param requests.Session session: A requests session object that can be used + for issuing requests. (optional) + :param string original_ip: The original IP of the requesting user which + will be sent to identity service in a + 'Forwarded' header. (optional) + :param verify: The verification arguments to pass to requests. These are of + the same form as requests expects, so True or False to + verify (or not) against system certificates or a path to a + bundle or CA certs to check against or None for requests to + attempt to locate and use certificates. (optional, defaults + to True) + :param cert: A client certificate to pass to requests. These are of the + same form as requests expects. Either a single filename + containing both the certificate and key or a tuple containing + the path to the certificate then a path to the key. (optional) + :param float timeout: A timeout to pass to requests. This should be a + numerical value indicating some amount (or fraction) + of seconds or 0 for no timeout. (optional, defaults + to 0) + :param string user_agent: A User-Agent header string to use for the + request. If not provided a default is used. + (optional, defaults to 'python-keystoneclient') + :param int/bool redirect: Controls the maximum number of redirections that + can be followed by a request. Either an integer + for a specific count or True/False for + forever/never. (optional, default to 30) + """ + + user_agent = None + + _REDIRECT_STATUSES = (301, 302, 303, 305, 307) + + REDIRECT_STATUSES = _REDIRECT_STATUSES + """This property is deprecated as of the 1.7.0 release and may be removed + in the 2.0.0 release.""" + + _DEFAULT_REDIRECT_LIMIT = 30 + + DEFAULT_REDIRECT_LIMIT = _DEFAULT_REDIRECT_LIMIT + """This property is deprecated as of the 1.7.0 release and may be removed + in the 2.0.0 release.""" + + def __init__(self, auth=None, session=None, original_ip=None, verify=True, + cert=None, timeout=None, user_agent=None, + redirect=_DEFAULT_REDIRECT_LIMIT): + warnings.warn( + 'keystoneclient.session.Session is deprecated as of the 2.1.0 ' + 'release in favor of keystoneauth1.session.Session. It will be ' + 'removed in future releases.', + DeprecationWarning) + + if not session: + session = requests.Session() + # Use TCPKeepAliveAdapter to fix bug 1323862 + for scheme in list(session.adapters): + session.mount(scheme, TCPKeepAliveAdapter()) + + self.auth = auth + self.session = session + self.original_ip = original_ip + self.verify = verify + self.cert = cert + self.timeout = None + self.redirect = redirect + + if timeout is not None: + self.timeout = float(timeout) + + # don't override the class variable if none provided + if user_agent is not None: + self.user_agent = user_agent + + @staticmethod + def _process_header(header): + """Redact the secure headers to be logged.""" + secure_headers = ('authorization', 'x-auth-token', + 'x-subject-token', 'x-service-token') + if header[0].lower() in secure_headers: + # hashlib.sha1() bandit nosec, as it is HMAC-SHA1 in + # keystone, which is considered secure (unlike just sha1) + token_hasher = hashlib.sha1() # nosec(lhinds) + token_hasher.update(header[1].encode('utf-8')) + token_hash = token_hasher.hexdigest() + return (header[0], '{SHA1}%s' % token_hash) + return header + + def _http_log_request(self, url, method=None, data=None, + headers=None, logger=_logger): + if not logger.isEnabledFor(logging.DEBUG): + # NOTE(morganfainberg): This whole debug section is expensive, + # there is no need to do the work if we're not going to emit a + # debug log. + return + + string_parts = ['REQ: curl -g -i'] + + # NOTE(jamielennox): None means let requests do its default validation + # so we need to actually check that this is False. + if self.verify is False: + string_parts.append('--insecure') + elif isinstance(self.verify, str): + string_parts.append('--cacert "%s"' % self.verify) + + if method: + string_parts.extend(['-X', method]) + + string_parts.append(url) + + if headers: + for header in headers.items(): + string_parts.append('-H "%s: %s"' + % self._process_header(header)) + + if data: + if isinstance(data, bytes): + try: + data = data.decode("ascii") + except UnicodeDecodeError: + data = "" + string_parts.append("-d '%s'" % data) + try: + logger.debug(' '.join(string_parts)) + except UnicodeDecodeError: + logger.debug("Replaced characters that could not be decoded" + " in log output, original caused UnicodeDecodeError") + string_parts = [ + encodeutils.safe_decode( + part, errors='replace') for part in string_parts] + logger.debug(' '.join(string_parts)) + + def _http_log_response(self, response, logger): + if not logger.isEnabledFor(logging.DEBUG): + return + + # NOTE(samueldmq): If the response does not provide enough info about + # the content type to decide whether it is useful and safe to log it + # or not, just do not log the body. Trying to# read the response body + # anyways may result on reading a long stream of bytes and getting an + # unexpected MemoryError. See bug 1616105 for further details. + content_type = response.headers.get('content-type', None) + + # NOTE(lamt): Per [1], the Content-Type header can be of the form + # Content-Type := type "/" subtype *[";" parameter] + # [1] https://www.w3.org/Protocols/rfc1341/4_Content-Type.html + for log_type in _LOG_CONTENT_TYPES: + if content_type is not None and content_type.startswith(log_type): + text = _remove_service_catalog(response.text) + break + else: + text = ('Omitted, Content-Type is set to %s. Only ' + '%s responses have their bodies logged.') + text = text % (content_type, ', '.join(_LOG_CONTENT_TYPES)) + + string_parts = [ + 'RESP:', + '[%s]' % response.status_code + ] + for header in response.headers.items(): + string_parts.append('%s: %s' % self._process_header(header)) + string_parts.append('\nRESP BODY: %s\n' % strutils.mask_password(text)) + + logger.debug(' '.join(string_parts)) + + # NOTE(artmr): parameter 'original_ip' value is never used + def request(self, url, method, json=None, original_ip=None, + user_agent=None, redirect=None, authenticated=None, + endpoint_filter=None, auth=None, requests_auth=None, + raise_exc=True, allow_reauth=True, log=True, + endpoint_override=None, connect_retries=0, logger=_logger, + **kwargs): + """Send an HTTP request with the specified characteristics. + + Wrapper around `requests.Session.request` to handle tasks such as + setting headers, JSON encoding/decoding, and error handling. + + Arguments that are not handled are passed through to the requests + library. + + :param string url: Path or fully qualified URL of HTTP request. If only + a path is provided then endpoint_filter must also be + provided such that the base URL can be determined. + If a fully qualified URL is provided then + endpoint_filter will be ignored. + :param string method: The http method to use. (e.g. 'GET', 'POST') + :param string original_ip: Mark this request as forwarded for this ip. + (optional) + :param dict headers: Headers to be included in the request. (optional) + :param json: Some data to be represented as JSON. (optional) + :param string user_agent: A user_agent to use for the request. If + present will override one present in headers. + (optional) + :param int/bool redirect: the maximum number of redirections that + can be followed by a request. Either an + integer for a specific count or True/False + for forever/never. (optional) + :param int connect_retries: the maximum number of retries that should + be attempted for connection errors. + (optional, defaults to 0 - never retry). + :param bool authenticated: True if a token should be attached to this + request, False if not or None for attach if + an auth_plugin is available. + (optional, defaults to None) + :param dict endpoint_filter: Data to be provided to an auth plugin with + which it should be able to determine an + endpoint to use for this request. If not + provided then URL is expected to be a + fully qualified URL. (optional) + :param str endpoint_override: The URL to use instead of looking up the + endpoint in the auth plugin. This will be + ignored if a fully qualified URL is + provided but take priority over an + endpoint_filter. (optional) + :param auth: The auth plugin to use when authenticating this request. + This will override the plugin that is attached to the + session (if any). (optional) + :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` + :param requests_auth: A requests library auth plugin that cannot be + passed via kwarg because the `auth` kwarg + collides with our own auth plugins. (optional) + :type requests_auth: :py:class:`requests.auth.AuthBase` + :param bool raise_exc: If True then raise an appropriate exception for + failed HTTP requests. If False then return the + request object. (optional, default True) + :param bool allow_reauth: Allow fetching a new token and retrying the + request on receiving a 401 Unauthorized + response. (optional, default True) + :param bool log: If True then log the request and response data to the + debug log. (optional, default True) + :param logger: The logger object to use to log request and responses. + If not provided the keystoneclient.session default + logger will be used. + :type logger: logging.Logger + :param kwargs: any other parameter that can be passed to + requests.Session.request (such as `headers`). Except: + 'data' will be overwritten by the data in 'json' param. + 'allow_redirects' is ignored as redirects are handled + by the session. + + :raises keystoneclient.exceptions.ClientException: For connection + failure, or to indicate an error response code. + + :returns: The response to the request. + """ + headers = kwargs.setdefault('headers', dict()) + + if authenticated is None: + authenticated = bool(auth or self.auth) + + if authenticated: + auth_headers = self.get_auth_headers(auth) + + if auth_headers is None: + msg = _('No valid authentication is available') + raise exceptions.AuthorizationFailure(msg) + + headers.update(auth_headers) + + if osprofiler_web: + headers.update(osprofiler_web.get_trace_id_headers()) + + # if we are passed a fully qualified URL and an endpoint_filter we + # should ignore the filter. This will make it easier for clients who + # want to overrule the default endpoint_filter data added to all client + # requests. We check fully qualified here by the presence of a host. + if not urllib.parse.urlparse(url).netloc: + base_url = None + + if endpoint_override: + base_url = endpoint_override + elif endpoint_filter: + base_url = self.get_endpoint(auth, **endpoint_filter) + + if not base_url: + service_type = (endpoint_filter or {}).get('service_type', + 'unknown') + msg = _('Endpoint for %s service') % service_type + raise exceptions.EndpointNotFound(msg) + + url = '%s/%s' % (base_url.rstrip('/'), url.lstrip('/')) + + if self.cert: + kwargs.setdefault('cert', self.cert) + + if self.timeout is not None: + kwargs.setdefault('timeout', self.timeout) + + if user_agent: + headers['User-Agent'] = user_agent + elif self.user_agent: + user_agent = headers.setdefault('User-Agent', self.user_agent) + else: + user_agent = headers.setdefault('User-Agent', USER_AGENT) + + if self.original_ip: + headers.setdefault('Forwarded', + 'for=%s;by=%s' % (self.original_ip, user_agent)) + + if json is not None: + headers['Content-Type'] = 'application/json' + kwargs['data'] = jsonutils.dumps(json) + + kwargs.setdefault('verify', self.verify) + + if requests_auth: + kwargs['auth'] = requests_auth + + if log: + self._http_log_request(url, method=method, + data=kwargs.get('data'), + headers=headers, + logger=logger) + + # Force disable requests redirect handling. We will manage this below. + kwargs['allow_redirects'] = False + + if redirect is None: + redirect = self.redirect + + send = functools.partial(self._send_request, + url, method, redirect, log, logger, + connect_retries) + + try: + connection_params = self.get_auth_connection_params(auth=auth) + except exceptions.MissingAuthPlugin: # nosec(cjschaef) + # NOTE(jamielennox): If we've gotten this far without an auth + # plugin then we should be happy with allowing no additional + # connection params. This will be the typical case for plugins + # anyway. + pass + else: + if connection_params: + kwargs.update(connection_params) + + resp = send(**kwargs) + + # handle getting a 401 Unauthorized response by invalidating the plugin + # and then retrying the request. This is only tried once. + if resp.status_code == 401 and authenticated and allow_reauth: + if self.invalidate(auth): + auth_headers = self.get_auth_headers(auth) + + if auth_headers is not None: + headers.update(auth_headers) + resp = send(**kwargs) + + if raise_exc and resp.status_code >= 400: + logger.debug('Request returned failure status: %s', + resp.status_code) + raise exceptions.from_response(resp, method, url) + + return resp + + def _send_request(self, url, method, redirect, log, logger, + connect_retries, connect_retry_delay=0.5, **kwargs): + # NOTE(jamielennox): We handle redirection manually because the + # requests lib follows some browser patterns where it will redirect + # POSTs as GETs for certain statuses which is not want we want for an + # API. See: https://en.wikipedia.org/wiki/Post/Redirect/Get + + # NOTE(jamielennox): The interaction between retries and redirects are + # handled naively. We will attempt only a maximum number of retries and + # redirects rather than per request limits. Otherwise the extreme case + # could be redirects * retries requests. This will be sufficient in + # most cases and can be fixed properly if there's ever a need. + + try: + try: + resp = self.session.request(method, url, **kwargs) + except requests.exceptions.SSLError as e: + msg = _('SSL exception connecting to %(url)s: ' + '%(error)s') % {'url': url, 'error': e} + raise exceptions.SSLError(msg) + except requests.exceptions.Timeout: + msg = _('Request to %s timed out') % url + raise exceptions.RequestTimeout(msg) + except requests.exceptions.ConnectionError: + msg = _('Unable to establish connection to %s') % url + raise exceptions.ConnectionRefused(msg) + except (exceptions.RequestTimeout, exceptions.ConnectionRefused) as e: + if connect_retries <= 0: + raise + + logger.info('Failure: %(e)s. Retrying in %(delay).1fs.', + {'e': e, 'delay': connect_retry_delay}) + time.sleep(connect_retry_delay) + + return self._send_request( + url, method, redirect, log, logger, + connect_retries=connect_retries - 1, + connect_retry_delay=connect_retry_delay * 2, + **kwargs) + + if log: + self._http_log_response(resp, logger) + + if resp.status_code in self._REDIRECT_STATUSES: + # be careful here in python True == 1 and False == 0 + if isinstance(redirect, bool): + redirect_allowed = redirect + else: + redirect -= 1 + redirect_allowed = redirect >= 0 + + if not redirect_allowed: + return resp + + try: + location = resp.headers['location'] + except KeyError: + logger.warning("Failed to redirect request to %s as new " + "location was not provided.", resp.url) + else: + # NOTE(jamielennox): We don't pass through connect_retry_delay. + # This request actually worked so we can reset the delay count. + new_resp = self._send_request( + location, method, redirect, log, logger, + connect_retries=connect_retries, + **kwargs) + + if not isinstance(new_resp.history, list): + new_resp.history = list(new_resp.history) + new_resp.history.insert(0, resp) + resp = new_resp + + return resp + + def head(self, url, **kwargs): + """Perform a HEAD request. + + This calls :py:meth:`.request()` with ``method`` set to ``HEAD``. + + """ + return self.request(url, 'HEAD', **kwargs) + + def get(self, url, **kwargs): + """Perform a GET request. + + This calls :py:meth:`.request()` with ``method`` set to ``GET``. + + """ + return self.request(url, 'GET', **kwargs) + + def post(self, url, **kwargs): + """Perform a POST request. + + This calls :py:meth:`.request()` with ``method`` set to ``POST``. + + """ + return self.request(url, 'POST', **kwargs) + + def put(self, url, **kwargs): + """Perform a PUT request. + + This calls :py:meth:`.request()` with ``method`` set to ``PUT``. + + """ + return self.request(url, 'PUT', **kwargs) + + def delete(self, url, **kwargs): + """Perform a DELETE request. + + This calls :py:meth:`.request()` with ``method`` set to ``DELETE``. + + """ + return self.request(url, 'DELETE', **kwargs) + + def patch(self, url, **kwargs): + """Perform a PATCH request. + + This calls :py:meth:`.request()` with ``method`` set to ``PATCH``. + + """ + return self.request(url, 'PATCH', **kwargs) + + @classmethod + def construct(cls, kwargs): + """Handle constructing a session from both old and new arguments. + + Support constructing a session from the old + :py:class:`~keystoneclient.httpclient.HTTPClient` args as well as the + new request-style arguments. + + .. warning:: + + *DEPRECATED as of 1.7.0*: This function is purely for bridging the + gap between older client arguments and the session arguments that + they relate to. It is not intended to be used as a generic Session + Factory. This function may be removed in the 2.0.0 release. + + This function purposefully modifies the input kwargs dictionary so that + the remaining kwargs dict can be reused and passed on to other + functions without session arguments. + + """ + warnings.warn( + 'Session.construct() is deprecated as of the 1.7.0 release in ' + 'favor of using session constructor and may be removed in the ' + '2.0.0 release.', DeprecationWarning) + return cls._construct(kwargs) + + @classmethod + def _construct(cls, kwargs): + params = {} + + for attr in ('verify', 'cacert', 'cert', 'key', 'insecure', + 'timeout', 'session', 'original_ip', 'user_agent'): + try: + params[attr] = kwargs.pop(attr) + except KeyError: # nosec(cjschaef): we are brute force + # identifying possible attributes for kwargs + pass + + return cls._make(**params) + + @classmethod + def _make(cls, 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: + warnings.warn( + 'Passing cert and key together is deprecated as of the 1.7.0 ' + 'release in favor of the requests library form of having the ' + 'cert and key as a tuple and may be removed in the 2.0.0 ' + 'release.', DeprecationWarning) + cert = (cert, key) + + return cls(verify=verify, cert=cert, **kwargs) + + def _auth_required(self, auth, msg): + if not auth: + auth = self.auth + + if not auth: + raise exceptions.MissingAuthPlugin(msg) + + return auth + + def get_auth_headers(self, auth=None, **kwargs): + """Return auth headers as provided by the auth plugin. + + :param auth: The auth plugin to use for token. Overrides the plugin + on the session. (optional) + :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` + + :raises keystoneclient.exceptions.AuthorizationFailure: if a new token + fetch fails. + :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not + available. + + :returns: Authentication headers or None for failure. + :rtype: dict + """ + msg = _('An auth plugin is required to fetch a token') + auth = self._auth_required(auth, msg) + return auth.get_headers(self, **kwargs) + + @removals.remove(message='Use get_auth_headers instead.', version='1.7.0', + removal_version='2.0.0') + def get_token(self, auth=None): + """Return a token as provided by the auth plugin. + + :param auth: The auth plugin to use for token. Overrides the plugin + on the session. (optional) + :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` + + :raises keystoneclient.exceptions.AuthorizationFailure: if a new token + fetch fails. + :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not + available. + + .. warning:: + + This method is deprecated as of the 1.7.0 release in favor of + :meth:`get_auth_headers` and may be removed in the 2.0.0 release. + This method assumes that the only header that is used to + authenticate a message is 'X-Auth-Token' which may not be correct. + + :returns: A valid token. + :rtype: string + """ + return (self.get_auth_headers(auth) or {}).get('X-Auth-Token') + + def get_endpoint(self, auth=None, **kwargs): + """Get an endpoint as provided by the auth plugin. + + :param auth: The auth plugin to use for token. Overrides the plugin on + the session. (optional) + :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` + + :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not + available. + + :returns: An endpoint if available or None. + :rtype: string + """ + msg = _('An auth plugin is required to determine endpoint URL') + auth = self._auth_required(auth, msg) + return auth.get_endpoint(self, **kwargs) + + def get_auth_connection_params(self, auth=None, **kwargs): + """Return auth connection params as provided by the auth plugin. + + An auth plugin may specify connection parameters to the request like + providing a client certificate for communication. + + We restrict the values that may be returned from this function to + prevent an auth plugin overriding values unrelated to connection + parameters. The values that are currently accepted are: + + - `cert`: a path to a client certificate, or tuple of client + certificate and key pair that are used with this request. + - `verify`: a boolean value to indicate verifying SSL certificates + against the system CAs or a path to a CA file to verify with. + + These values are passed to the requests library and further information + on accepted values may be found there. + + :param auth: The auth plugin to use for tokens. Overrides the plugin + on the session. (optional) + :type auth: keystoneclient.auth.base.BaseAuthPlugin + + :raises keystoneclient.exceptions.AuthorizationFailure: if a new token + fetch fails. + :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not + available. + :raises keystoneclient.exceptions.UnsupportedParameters: if the plugin + returns a parameter that is not supported by this session. + + :returns: Authentication headers or None for failure. + :rtype: dict + """ + msg = _('An auth plugin is required to fetch connection params') + auth = self._auth_required(auth, msg) + params = auth.get_connection_params(self, **kwargs) + + # NOTE(jamielennox): There needs to be some consensus on what + # parameters are allowed to be modified by the auth plugin here. + # Ideally I think it would be only the send() parts of the request + # flow. For now lets just allow certain elements. + params_copy = params.copy() + + for arg in ('cert', 'verify'): + try: + kwargs[arg] = params_copy.pop(arg) + except KeyError: # nosec(cjschaef): we are brute force + # identifying and removing values in params_copy + pass + + if params_copy: + raise exceptions.UnsupportedParameters(list(params_copy)) + + return params + + def invalidate(self, auth=None): + """Invalidate an authentication plugin. + + :param auth: The auth plugin to invalidate. Overrides the plugin on the + session. (optional) + :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` + + """ + msg = _('An auth plugin is required to validate') + auth = self._auth_required(auth, msg) + return auth.invalidate() + + def get_user_id(self, auth=None): + """Return the authenticated user_id as provided by the auth plugin. + + :param auth: The auth plugin to use for token. Overrides the plugin + on the session. (optional) + :type auth: keystoneclient.auth.base.BaseAuthPlugin + + :raises keystoneclient.exceptions.AuthorizationFailure: + if a new token fetch fails. + :raises keystoneclient.exceptions.MissingAuthPlugin: + if a plugin is not available. + + :returns string: Current user_id or None if not supported by plugin. + """ + msg = _('An auth plugin is required to get user_id') + auth = self._auth_required(auth, msg) + return auth.get_user_id(self) + + def get_project_id(self, auth=None): + """Return the authenticated project_id as provided by the auth plugin. + + :param auth: The auth plugin to use for token. Overrides the plugin + on the session. (optional) + :type auth: keystoneclient.auth.base.BaseAuthPlugin + + :raises keystoneclient.exceptions.AuthorizationFailure: + if a new token fetch fails. + :raises keystoneclient.exceptions.MissingAuthPlugin: + if a plugin is not available. + + :returns string: Current project_id or None if not supported by plugin. + """ + msg = _('An auth plugin is required to get project_id') + auth = self._auth_required(auth, msg) + return auth.get_project_id(self) + + @classmethod + def get_conf_options(cls, deprecated_opts=None): + """Get oslo_config options that are needed for a :py:class:`.Session`. + + These may be useful without being registered for config file generation + or to manipulate the options before registering them yourself. + + The options that are set are: + :cafile: The certificate authority filename. + :certfile: The client certificate file to present. + :keyfile: The key for the client certificate. + :insecure: Whether to ignore SSL verification. + :timeout: The max time to wait for HTTP connections. + + :param dict deprecated_opts: Deprecated options that should be included + in the definition of new options. This should be a dict from the + name of the new option to a list of oslo.DeprecatedOpts that + correspond to the new option. (optional) + + For example, to support the ``ca_file`` option pointing to the new + ``cafile`` option name:: + + old_opt = oslo_cfg.DeprecatedOpt('ca_file', 'old_group') + deprecated_opts={'cafile': [old_opt]} + + :returns: A list of oslo_config options. + """ + if 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'), + ] + + @classmethod + def register_conf_options(cls, 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 = cls.get_conf_options(deprecated_opts=deprecated_opts) + conf.register_group(cfg.OptGroup(group)) + conf.register_opts(opts, group=group) + return opts + + @classmethod + def load_from_conf_options(cls, 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['insecure'] = c.insecure + kwargs['cacert'] = c.cafile + if c.certfile and c.keyfile: + kwargs['cert'] = (c.certfile, c.keyfile) + kwargs['timeout'] = c.timeout + + return cls._make(**kwargs) + + @staticmethod + def register_cli_options(parser): + """Register the argparse arguments that are needed for a session. + + :param argparse.ArgumentParser parser: parser to add to. + """ + parser.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.') + + parser.add_argument('--os-cacert', + metavar='', + default=os.environ.get('OS_CACERT'), + help='Specify a CA bundle file to use in ' + 'verifying a TLS (https) server certificate. ' + 'Defaults to env[OS_CACERT].') + + parser.add_argument('--os-cert', + metavar='', + default=os.environ.get('OS_CERT'), + help='Defaults to env[OS_CERT].') + + parser.add_argument('--os-key', + metavar='', + default=os.environ.get('OS_KEY'), + help='Defaults to env[OS_KEY].') + + parser.add_argument('--timeout', + default=600, + type=_positive_non_zero_float, + metavar='', + help='Set request timeout (in seconds).') + + @classmethod + def load_from_cli_options(cls, args, **kwargs): + """Create a :py:class:`.Session` object from CLI arguments. + + The CLI arguments must have been registered with + :py:meth:`.register_cli_options`. + + :param Namespace args: result of parsed arguments. + + :returns: A new session object. + :rtype: :py:class:`.Session` + """ + kwargs['insecure'] = args.insecure + kwargs['cacert'] = args.os_cacert + if args.os_cert and args.os_key: + kwargs['cert'] = (args.os_cert, args.os_key) + kwargs['timeout'] = args.timeout + + return cls._make(**kwargs) + + +class TCPKeepAliveAdapter(requests.adapters.HTTPAdapter): + """The custom adapter used to set TCP Keep-Alive on all connections. + + This Adapter also preserves the default behaviour of Requests which + disables Nagle's Algorithm. See also: + http://blogs.msdn.com/b/windowsazurestorage/archive/2010/06/25/nagle-s-algorithm-is-not-friendly-towards-small-requests.aspx + """ + + def init_poolmanager(self, *args, **kwargs): + if 'socket_options' not in kwargs: + socket_options = [ + # Keep Nagle's algorithm off + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), + # Turn on TCP Keep-Alive + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ] + + # Some operating systems (e.g., OSX) do not support setting + # keepidle + if hasattr(socket, 'TCP_KEEPIDLE'): + socket_options += [ + # Wait 60 seconds before sending keep-alive probes + (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) + ] + + # TODO(claudiub): Windows does not contain the TCP_KEEPCNT and + # TCP_KEEPINTVL socket attributes. Instead, it contains + # SIO_KEEPALIVE_VALS, which can be set via ioctl, which should be + # set once it is available in requests. + # https://msdn.microsoft.com/en-us/library/dd877220%28VS.85%29.aspx + if hasattr(socket, 'TCP_KEEPCNT'): + socket_options += [ + # Set the maximum number of keep-alive probes + (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4) + ] + + if hasattr(socket, 'TCP_KEEPINTVL'): + socket_options += [ + # Send keep-alive probes every 15 seconds + (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15) + ] + + # After waiting 60 seconds, and then sending a probe once every 15 + # seconds 4 times, these options should ensure that a connection + # hands for no longer than 2 minutes before a ConnectionError is + # raised. + + kwargs['socket_options'] = socket_options + super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs) diff --git a/keystoneclient/shell.py b/keystoneclient/shell.py deleted file mode 100644 index ae8870f41..000000000 --- a/keystoneclient/shell.py +++ /dev/null @@ -1,477 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Command-line interface to the OpenStack Identity API. -""" - -import argparse -import getpass -import os -import sys - -import keystoneclient - -from keystoneclient import access -from keystoneclient import exceptions as exc -from keystoneclient import utils -from keystoneclient.v2_0 import shell as shell_v2_0 -from keystoneclient.generic import shell as shell_generic -from keystoneclient.contrib.bootstrap import shell as shell_bootstrap - - -def env(*vars, **kwargs): - """Search for the first defined of possibly many env vars - - Returns the first environment variable defined in vars, or - returns the default defined in kwargs. - - """ - for v in vars: - value = os.environ.get(v, None) - if value: - return value - return kwargs.get('default', '') - - -class OpenStackIdentityShell(object): - - def get_base_parser(self): - parser = argparse.ArgumentParser( - prog='keystone', - description=__doc__.strip(), - epilog='See "keystone help COMMAND" ' - 'for help on a specific command.', - add_help=False, - formatter_class=OpenStackHelpFormatter, - ) - - # Global arguments - parser.add_argument('-h', - '--help', - action='store_true', - help=argparse.SUPPRESS) - - parser.add_argument('--version', - action='version', - version=keystoneclient.__version__) - - parser.add_argument('--debug', - default=False, - action='store_true', - help=argparse.SUPPRESS) - - parser.add_argument('--os-username', - metavar='', - default=env('OS_USERNAME'), - help='Name used for authentication with the ' - 'OpenStack Identity service. ' - 'Defaults to env[OS_USERNAME]') - parser.add_argument('--os_username', - help=argparse.SUPPRESS) - - parser.add_argument('--os-password', - metavar='', - default=env('OS_PASSWORD'), - help='Password used for authentication with the ' - 'OpenStack Identity service. ' - 'Defaults to env[OS_PASSWORD]') - parser.add_argument('--os_password', - help=argparse.SUPPRESS) - - parser.add_argument('--os-tenant-name', - metavar='', - default=env('OS_TENANT_NAME'), - help='Tenant to request authorization on. ' - 'Defaults to env[OS_TENANT_NAME]') - parser.add_argument('--os_tenant_name', - help=argparse.SUPPRESS) - - parser.add_argument('--os-tenant-id', - metavar='', - default=env('OS_TENANT_ID'), - help='Tenant to request authorization on. ' - 'Defaults to env[OS_TENANT_ID]') - parser.add_argument('--os_tenant_id', - help=argparse.SUPPRESS) - - parser.add_argument('--os-auth-url', - metavar='', - default=env('OS_AUTH_URL'), - help='Specify the Identity endpoint to use for ' - 'authentication. ' - 'Defaults to env[OS_AUTH_URL]') - parser.add_argument('--os_auth_url', - help=argparse.SUPPRESS) - - parser.add_argument('--os-region-name', - metavar='', - default=env('OS_REGION_NAME'), - help='Defaults to env[OS_REGION_NAME]') - parser.add_argument('--os_region_name', - help=argparse.SUPPRESS) - - parser.add_argument('--os-identity-api-version', - metavar='', - default=env('OS_IDENTITY_API_VERSION', - 'KEYSTONE_VERSION'), - help='Defaults to env[OS_IDENTITY_API_VERSION]' - ' or 2.0') - parser.add_argument('--os_identity_api_version', - help=argparse.SUPPRESS) - - parser.add_argument('--os-token', - metavar='', - default=env('OS_SERVICE_TOKEN'), - help='Specify an existing token to use instead of ' - 'retrieving one via authentication (e.g. ' - 'with username & password). ' - 'Defaults to env[OS_SERVICE_TOKEN]') - - parser.add_argument('--os-endpoint', - metavar='', - default=env('OS_SERVICE_ENDPOINT'), - help='Specify an endpoint to use instead of ' - 'retrieving one from the service catalog ' - '(via authentication). ' - 'Defaults to env[OS_SERVICE_ENDPOINT]') - - parser.add_argument('--os-cacert', - metavar='', - default=env('OS_CACERT', default=None), - help='Specify a CA bundle file to use in ' - 'verifying a TLS (https) server certificate. ' - 'Defaults to env[OS_CACERT]') - parser.add_argument('--os_cacert', - help=argparse.SUPPRESS) - - parser.add_argument('--insecure', - default=False, - action="store_true", - help='Explicitly allow keystoneclient 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.') - - parser.add_argument('--os-cert', - metavar='', - default=env('OS_CERT'), - help='Defaults to env[OS_CERT]') - parser.add_argument('--os_cert', - help=argparse.SUPPRESS) - - parser.add_argument('--os-key', - metavar='', - default=env('OS_KEY'), - help='Defaults to env[OS_KEY]') - parser.add_argument('--os_key', - help=argparse.SUPPRESS) - - parser.add_argument('--os-cache', - default=env('OS_CACHE', default=False), - action='store_true', - help='Use the auth token cache. ' - 'Default to env[OS_CACHE]') - parser.add_argument('--os_cache', - help=argparse.SUPPRESS) - - parser.add_argument('--force-new-token', - default=False, - action="store_true", - dest='force_new_token', - help="If keyring is available and in used, " - "token will always be stored and fetched " - "from the keyring, until the token has " - "expired. Use this option to request a " - "new token and replace the existing one " - "in keyring.") - - parser.add_argument('--stale-duration', - metavar='', - default=access.STALE_TOKEN_DURATION, - dest='stale_duration', - help="Stale duration (in seconds) used to " - "determine whether a token has expired " - "when retrieving it from keyring. This " - "is useful in mitigating process or " - "network delays. Default is %s seconds." % ( - access.STALE_TOKEN_DURATION)) - - #FIXME(heckj): - # deprecated command line options for essex compatibility. To be - # removed in Grizzly release cycle. - parser.add_argument('--token', - metavar='', - dest='os_token', - default=env('SERVICE_TOKEN'), - help=argparse.SUPPRESS) - parser.add_argument('--endpoint', - dest='os_endpoint', - metavar='', - default=env('SERVICE_ENDPOINT'), - help=argparse.SUPPRESS) - - return parser - - def get_subcommand_parser(self, version): - parser = self.get_base_parser() - - self.subcommands = {} - subparsers = parser.add_subparsers(metavar='') - - try: - actions_module = { - '2.0': shell_v2_0, - }[version] - except KeyError: - actions_module = shell_v2_0 - - self._find_actions(subparsers, actions_module) - self._find_actions(subparsers, shell_generic) - self._find_actions(subparsers, shell_bootstrap) - self._find_actions(subparsers, self) - self._add_bash_completion_subparser(subparsers) - - return parser - - def _add_bash_completion_subparser(self, subparsers): - subparser = subparsers.add_parser( - 'bash_completion', - add_help=False, - formatter_class=OpenStackHelpFormatter - ) - self.subcommands['bash_completion'] = subparser - subparser.set_defaults(func=self.do_bash_completion) - - def _find_actions(self, subparsers, actions_module): - for attr in (a for a in dir(actions_module) if a.startswith('do_')): - # I prefer to be hypen-separated instead of underscores. - command = attr[3:].replace('_', '-') - callback = getattr(actions_module, attr) - desc = callback.__doc__ or '' - help = desc.strip().split('\n')[0] - arguments = getattr(callback, 'arguments', []) - - subparser = subparsers.add_parser( - command, - help=help, - description=desc, - add_help=False, - formatter_class=OpenStackHelpFormatter) - subparser.add_argument('-h', '--help', action='help', - help=argparse.SUPPRESS) - self.subcommands[command] = subparser - for (args, kwargs) in arguments: - subparser.add_argument(*args, **kwargs) - subparser.set_defaults(func=callback) - - def main(self, argv): - # Parse args once to find version - parser = self.get_base_parser() - (options, args) = parser.parse_known_args(argv) - - # build available subcommands based on version - api_version = options.os_identity_api_version - subcommand_parser = self.get_subcommand_parser(api_version) - self.parser = subcommand_parser - - # Handle top-level --help/-h before attempting to parse - # a command off the command line - if not argv or options.help: - self.do_help(options) - return 0 - - # Parse args again and call whatever callback was selected - args = subcommand_parser.parse_args(argv) - - # Short-circuit and deal with help command right away. - if args.func == self.do_help: - self.do_help(args) - return 0 - elif args.func == self.do_bash_completion: - self.do_bash_completion(args) - return 0 - - # TODO(heckj): supporting backwards compatibility with environment - # variables. To be removed after DEVSTACK is updated, ideally in - # the Grizzly release cycle. - args.os_token = args.os_token or env('SERVICE_TOKEN') - args.os_endpoint = args.os_endpoint or env('SERVICE_ENDPOINT') - - if not utils.isunauthenticated(args.func): - # if the user hasn't provided any auth data - if not (args.os_token or args.os_endpoint or args.os_username or - args.os_password or args.os_auth_url): - raise exc.CommandError('Expecting authentication method via' - '\n either a service token, ' - '--os-token or env[OS_SERVICE_TOKEN], ' - '\n or credentials, ' - '--os-username or env[OS_USERNAME].') - - # user supplied a token and endpoint and at least one other cred - if (args.os_token and args.os_endpoint) and (args.os_username or - args.os_tenant_id or - args.os_tenant_name or - args.os_password or - args.os_auth_url): - msg = ('WARNING: Bypassing authentication using a token & ' - 'endpoint (authentication credentials are being ' - 'ignored).') - print msg - - # if it looks like the user wants to provide a credentials - # but is missing something - if (not (args.os_token and args.os_endpoint) - and ((args.os_username or args.os_password or args.os_auth_url) - and not (args.os_username and args.os_password and - args.os_auth_url))): - if not args.os_username: - raise exc.CommandError( - 'Expecting a username provided via either ' - '--os-username or env[OS_USERNAME]') - - if not args.os_auth_url: - raise exc.CommandError( - 'Expecting an auth URL via either --os-auth-url or ' - 'env[OS_AUTH_URL]') - - if not args.os_password: - # No password, If we've got a tty, try prompting for it - if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): - # Check for Ctl-D - try: - args.os_password = getpass.getpass('OS Password: ') - except EOFError: - pass - # No password because we did't have a tty or the - # user Ctl-D when prompted? - if not args.os_password: - raise exc.CommandError( - 'Expecting a password provided via either ' - '--os-password, env[OS_PASSWORD], or ' - 'prompted response') - - # if it looks like the user wants to provide a service token - # but is missing something - if args.os_token or args.os_endpoint and not ( - args.os_token and args.os_endpoint): - if not args.os_token: - raise exc.CommandError( - 'Expecting a token provided via either --os-token or ' - 'env[OS_SERVICE_TOKEN]') - - if not args.os_endpoint: - raise exc.CommandError( - 'Expecting an endpoint provided via either ' - '--os-endpoint or env[OS_SERVICE_ENDPOINT]') - - if utils.isunauthenticated(args.func): - self.cs = shell_generic.CLIENT_CLASS(endpoint=args.os_auth_url, - cacert=args.os_cacert, - key=args.os_key, - cert=args.os_cert, - insecure=args.insecure, - debug=args.debug) - else: - token = None - if args.os_token and args.os_endpoint: - token = args.os_token - api_version = options.os_identity_api_version - self.cs = self.get_api_class(api_version)( - username=args.os_username, - tenant_name=args.os_tenant_name, - tenant_id=args.os_tenant_id, - token=token, - endpoint=args.os_endpoint, - password=args.os_password, - auth_url=args.os_auth_url, - region_name=args.os_region_name, - cacert=args.os_cacert, - key=args.os_key, - cert=args.os_cert, - insecure=args.insecure, - debug=args.debug, - use_keyring=args.os_cache, - force_new_token=args.force_new_token, - stale_duration=args.stale_duration) - - try: - args.func(self.cs, args) - except exc.Unauthorized: - raise exc.CommandError("Invalid OpenStack Identity credentials.") - except exc.AuthorizationFailure: - raise exc.CommandError("Unable to authorize user") - - def get_api_class(self, version): - try: - return { - "2.0": shell_v2_0.CLIENT_CLASS, - }[version] - except KeyError: - return shell_v2_0.CLIENT_CLASS - - def do_bash_completion(self, args): - """ - Prints all of the commands and options to stdout. - The keystone.bash_completion script doesn't have to hard code them. - """ - commands = set() - options = set() - for sc_str, sc in self.subcommands.items(): - commands.add(sc_str) - for option in sc._optionals._option_string_actions.keys(): - options.add(option) - - commands.remove('bash-completion') - commands.remove('bash_completion') - print ' '.join(commands | options) - - @utils.arg('command', metavar='', nargs='?', - help='Display help for ') - def do_help(self, args): - """ - Display help about this program or one of its subcommands. - """ - if getattr(args, 'command', None): - if args.command in self.subcommands: - self.subcommands[args.command].print_help() - else: - raise exc.CommandError("'%s' is not a valid subcommand" % - args.command) - else: - self.parser.print_help() - - -# I'm picky about my shell help. -class OpenStackHelpFormatter(argparse.HelpFormatter): - def start_section(self, heading): - # Title-case the headings - heading = '%s%s' % (heading[0].upper(), heading[1:]) - super(OpenStackHelpFormatter, self).start_section(heading) - - -def main(): - try: - OpenStackIdentityShell().main(sys.argv[1:]) - - except Exception as e: - print >> sys.stderr, e - sys.exit(1) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/keystoneclient/openstack/__init__.py b/keystoneclient/tests/__init__.py similarity index 100% rename from keystoneclient/openstack/__init__.py rename to keystoneclient/tests/__init__.py diff --git a/keystoneclient/openstack/common/__init__.py b/keystoneclient/tests/functional/__init__.py similarity index 100% rename from keystoneclient/openstack/common/__init__.py rename to keystoneclient/tests/functional/__init__.py diff --git a/keystoneclient/tests/functional/base.py b/keystoneclient/tests/functional/base.py new file mode 100644 index 000000000..76355099f --- /dev/null +++ b/keystoneclient/tests/functional/base.py @@ -0,0 +1,88 @@ +# 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 openstack import config as occ +import openstack.exceptions +import testtools + +from keystoneclient import client + +IDENTITY_CLIENT = 'identity' +OPENSTACK_CLOUDS = ('functional_admin', 'devstack-admin', 'envvars') + + +def get_client(version): + """Create a keystoneclient instance to run functional tests. + + The client is instantiated either based on a clouds.yaml config file or + from the environment variables. + + First, look for a 'functional_admin' cloud, as this is a cloud that the + user may have defined for functional testing with admin credentials. If + that is not found, check for the 'devstack-admin' cloud. Finally, fall + back to looking for environment variables. + + """ + for cloud in OPENSTACK_CLOUDS: + try: + cloud_config = occ.OpenStackConfig().get_one( + cloud=cloud, identity_api_version=version) + endpoint = cloud_config.get_session_endpoint(IDENTITY_CLIENT) + return client.Client( + version=version, + session=cloud_config.get_session(), + endpoint=endpoint) + except openstack.exceptions.ConfigException: + pass + + raise Exception("Could not find any cloud definition for clouds named" + " functional_admin or devstack-admin. Check your" + " clouds.yaml file or your envvars and try again.") + + +class ClientTestCase(testtools.TestCase): + + def setUp(self): + super(ClientTestCase, self).setUp() + + if not self.auth_ref.project_scoped: + raise Exception("Could not run functional tests, which are " + "run based on the scope provided for " + "authentication. Please provide a project " + "scope information.") + + @property + def client(self): + if not hasattr(self, '_client'): + self._client = get_client(self.version) + + return self._client + + @property + def auth_ref(self): + return self.client.session.auth.get_auth_ref(self.client.session) + + @property + def project_domain_id(self): + return self.auth_ref.project_domain_id + + @property + def project_id(self): + return self.client.session.get_project_id() + + @property + def user_id(self): + return self.client.session.get_user_id() + + +class V3ClientTestCase(ClientTestCase): + version = '3' diff --git a/keystoneclient/tests/functional/test_base.py b/keystoneclient/tests/functional/test_base.py new file mode 100644 index 000000000..091fc77d4 --- /dev/null +++ b/keystoneclient/tests/functional/test_base.py @@ -0,0 +1,21 @@ +# 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 keystoneclient +from keystoneclient.tests.functional import base + + +class V3ClientVersionTestCase(base.V3ClientTestCase): + + def test_version(self): + self.assertIsInstance(self.client, + keystoneclient.v3.client.Client) diff --git a/tests/__init__.py b/keystoneclient/tests/functional/v3/__init__.py similarity index 100% rename from tests/__init__.py rename to keystoneclient/tests/functional/v3/__init__.py diff --git a/keystoneclient/tests/functional/v3/client_fixtures.py b/keystoneclient/tests/functional/v3/client_fixtures.py new file mode 100644 index 000000000..edffc9b8c --- /dev/null +++ b/keystoneclient/tests/functional/v3/client_fixtures.py @@ -0,0 +1,252 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import fixtures +import uuid + + +RESOURCE_NAME_PREFIX = 'keystoneclient-functional-' + + +class Base(fixtures.Fixture): + + def __init__(self, client, domain_id=None): + super(Base, self).__init__() + + self.client = client + self.domain_id = domain_id + self.ref = None + self.entity = None + + def __getattr__(self, name): + """Return the attribute from the represented entity.""" + return getattr(self.entity, name) + + +class User(Base): + + def setUp(self): + super(User, self).setUp() + + self.ref = {'name': RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.domain_id} + self.entity = self.client.users.create(**self.ref) + self.addCleanup(self.client.users.delete, self.entity) + + +class Group(Base): + + def setUp(self): + super(Group, self).setUp() + + self.ref = {'name': RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.domain_id} + self.entity = self.client.groups.create(**self.ref) + self.addCleanup(self.client.groups.delete, self.entity) + + +class Domain(Base): + + def setUp(self): + super(Domain, self).setUp() + + self.ref = {'name': RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True} + self.entity = self.client.domains.create(**self.ref) + + # Only disabled domains can be deleted + self.addCleanup(self.client.domains.delete, self.entity) + self.addCleanup(self.client.domains.update, self.entity, enabled=False) + + +class Project(Base): + + def __init__(self, client, domain_id=None, parent=None, tags=None): + super(Project, self).__init__(client, domain_id) + self.parent = parent + self.tags = tags if tags else [] + + def setUp(self): + super(Project, self).setUp() + + self.ref = {'name': RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.domain_id, + 'enabled': True, + 'parent': self.parent, + 'tags': self.tags} + self.entity = self.client.projects.create(**self.ref) + self.addCleanup(self.client.projects.delete, self.entity) + + +class Role(Base): + + def __init__(self, client, name=None, domain=None): + super(Role, self).__init__(client) + self.name = name or RESOURCE_NAME_PREFIX + uuid.uuid4().hex + self.domain = domain + + def setUp(self): + super(Role, self).setUp() + + self.ref = {'name': self.name, + 'domain': self.domain} + self.entity = self.client.roles.create(**self.ref) + self.addCleanup(self.client.roles.delete, self.entity) + + +class InferenceRule(Base): + + def __init__(self, client, prior_role, implied_role): + super(InferenceRule, self).__init__(client) + self.prior_role = prior_role + self.implied_role = implied_role + + def setUp(self): + super(InferenceRule, self).setUp() + + self.ref = {'prior_role': self.prior_role, + 'implied_role': self.implied_role} + self.entity = self.client.inference_rules.create(**self.ref) + self.addCleanup(self.client.inference_rules.delete, self.prior_role, + self.implied_role) + + +class Service(Base): + + def setUp(self): + super(Service, self).setUp() + + self.ref = {'name': RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'enabled': True, + 'description': uuid.uuid4().hex} + self.entity = self.client.services.create(**self.ref) + self.addCleanup(self.client.services.delete, self.entity) + + +class Policy(Base): + + def setUp(self): + super(Policy, self).setUp() + + self.ref = {'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex} + self.entity = self.client.policies.create(**self.ref) + self.addCleanup(self.client.policies.delete, self.entity) + + +class Region(Base): + + def __init__(self, client, parent_region=None): + super(Region, self).__init__(client) + self.parent_region = parent_region + + def setUp(self): + super(Region, self).setUp() + + self.ref = {'description': uuid.uuid4().hex, + 'parent_region': self.parent_region} + self.entity = self.client.regions.create(**self.ref) + self.addCleanup(self.client.regions.delete, self.entity) + + +class Endpoint(Base): + + def __init__(self, client, service, interface, region=None): + super(Endpoint, self).__init__(client) + self.service = service + self.interface = interface + self.region = region + + def setUp(self): + super(Endpoint, self).setUp() + + self.ref = {'service': self.service, + 'url': 'http://' + uuid.uuid4().hex, + 'enabled': True, + 'interface': self.interface, + 'region': self.region} + self.entity = self.client.endpoints.create(**self.ref) + self.addCleanup(self.client.endpoints.delete, self.entity) + + +class EndpointGroup(Base): + + def setUp(self): + super(EndpointGroup, self).setUp() + + self.ref = {'name': RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'filters': {'interface': 'public'}, + 'description': uuid.uuid4().hex} + self.entity = self.client.endpoint_groups.create(**self.ref) + self.addCleanup(self.client.endpoint_groups.delete, self.entity) + + +class Credential(Base): + + def __init__(self, client, user, type, project=None): + super(Credential, self).__init__(client) + self.user = user + self.type = type + self.project = project + + if type == 'ec2': + self.blob = ("{\"access\":\"" + uuid.uuid4().hex + + "\",\"secret\":\"secretKey\"}") + else: + self.blob = uuid.uuid4().hex + + def setUp(self): + super(Credential, self).setUp() + + self.ref = {'user': self.user, + 'type': self.type, + 'blob': self.blob, + 'project': self.project} + self.entity = self.client.credentials.create(**self.ref) + self.addCleanup(self.client.credentials.delete, self.entity) + + +class EC2(Base): + + def __init__(self, client, user_id, project_id): + super(EC2, self).__init__(client) + self.user_id = user_id + self.project_id = project_id + + def setUp(self): + super(EC2, self).setUp() + + self.ref = {'user_id': self.user_id, + 'project_id': self.project_id} + self.entity = self.client.ec2.create(**self.ref) + self.addCleanup(self.client.ec2.delete, + self.user_id, + self.entity.access) + + +class DomainConfig(Base): + + def __init__(self, client, domain_id): + super(DomainConfig, self).__init__(client, domain_id=domain_id) + self.domain_id = domain_id + + def setUp(self): + super(DomainConfig, self).setUp() + + self.ref = {'identity': {'driver': uuid.uuid4().hex}, + 'ldap': {'url': uuid.uuid4().hex}} + self.entity = self.client.domain_configs.create( + self.domain_id, self.ref) + self.addCleanup(self.client.domain_configs.delete, + self.domain_id) diff --git a/keystoneclient/tests/functional/v3/test_credentials.py b/keystoneclient/tests/functional/v3/test_credentials.py new file mode 100644 index 000000000..a5d00b1c0 --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_credentials.py @@ -0,0 +1,194 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class CredentialsTestCase(base.V3ClientTestCase): + + def setUp(self): + super(CredentialsTestCase, self).setUp() + self.test_domain = fixtures.Domain(self.client) + self.useFixture(self.test_domain) + + def check_credential(self, credential, credential_ref=None): + self.assertIsNotNone(credential.id) + self.assertIn('self', credential.links) + self.assertIn('/credentials/' + credential.id, + credential.links['self']) + + if credential_ref: + self.assertEqual(credential_ref['user'], credential.user_id) + self.assertEqual(credential_ref['type'], credential.type) + self.assertEqual(credential_ref['blob'], credential.blob) + + # There is no guarantee below attributes are present in credential + if credential_ref['type'] == 'ec2' or hasattr(credential_ref, + 'project'): + self.assertEqual(credential_ref['project'], + credential.project_id) + + else: + # Only check remaining mandatory attributes + self.assertIsNotNone(credential.user_id) + self.assertIsNotNone(credential.type) + self.assertIsNotNone(credential.blob) + if credential.type == 'ec2': + self.assertIsNotNone(credential.project_id) + + def test_create_credential_of_cert_type(self): + user = fixtures.User(self.client, self.test_domain.id) + self.useFixture(user) + + credential_ref = {'user': user.id, + 'type': 'cert', + 'blob': uuid.uuid4().hex} + credential = self.client.credentials.create(**credential_ref) + + self.addCleanup(self.client.credentials.delete, credential) + self.check_credential(credential, credential_ref) + + def test_create_credential_of_ec2_type(self): + user = fixtures.User(self.client, self.test_domain.id) + self.useFixture(user) + + # project is mandatory attribute if the credential type is ec2 + credential_ref = {'user': user.id, + 'type': 'ec2', + 'blob': ("{\"access\":\"" + uuid.uuid4().hex + + "\",\"secret\":\"secretKey\"}")} + self.assertRaises(http.BadRequest, + self.client.credentials.create, + **credential_ref) + + project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(project) + + credential_ref = {'user': user.id, + 'type': 'ec2', + 'blob': ("{\"access\":\"" + uuid.uuid4().hex + + "\",\"secret\":\"secretKey\"}"), + 'project': project.id} + credential = self.client.credentials.create(**credential_ref) + + self.addCleanup(self.client.credentials.delete, credential) + self.check_credential(credential, credential_ref) + + def test_create_credential_of_totp_type(self): + user = fixtures.User(self.client, self.test_domain.id) + self.useFixture(user) + + credential_ref = {'user': user.id, + 'type': 'totp', + 'blob': uuid.uuid4().hex} + credential = self.client.credentials.create(**credential_ref) + + self.addCleanup(self.client.credentials.delete, credential) + self.check_credential(credential, credential_ref) + + def test_get_credential(self): + user = fixtures.User(self.client, self.test_domain.id) + self.useFixture(user) + project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(project) + + for credential_type in ['cert', 'ec2', 'totp']: + credential = fixtures.Credential(self.client, user=user.id, + type=credential_type, + project=project.id) + self.useFixture(credential) + + credential_ret = self.client.credentials.get(credential.id) + self.check_credential(credential_ret, credential.ref) + + def test_list_credentials(self): + user = fixtures.User(self.client, self.test_domain.id) + self.useFixture(user) + + cert_credential = fixtures.Credential(self.client, user=user.id, + type='cert') + self.useFixture(cert_credential) + + project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(project) + ec2_credential = fixtures.Credential(self.client, user=user.id, + type='ec2', project=project.id) + self.useFixture(ec2_credential) + + totp_credential = fixtures.Credential(self.client, user=user.id, + type='totp') + self.useFixture(totp_credential) + + credentials = self.client.credentials.list() + + # All credentials are valid + for credential in credentials: + self.check_credential(credential) + + self.assertIn(cert_credential.entity, credentials) + self.assertIn(ec2_credential.entity, credentials) + self.assertIn(totp_credential.entity, credentials) + + def test_update_credential(self): + user = fixtures.User(self.client, self.test_domain.id) + self.useFixture(user) + + new_user = fixtures.User(self.client, self.test_domain.id) + self.useFixture(new_user) + new_project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(new_project) + + credential = fixtures.Credential(self.client, user=user.id, + type='cert') + self.useFixture(credential) + + new_type = 'ec2' + new_blob = ("{\"access\":\"" + uuid.uuid4().hex + + "\",\"secret\":\"secretKey\"}") + + credential_ret = self.client.credentials.update(credential.id, + user=new_user.id, + type=new_type, + blob=new_blob, + project=new_project.id) + + credential.ref.update({'user': new_user.id, 'type': new_type, + 'blob': new_blob, 'project': new_project.id}) + self.check_credential(credential_ret, credential.ref) + + def test_delete_credential(self): + user = fixtures.User(self.client, self.test_domain.id) + self.useFixture(user) + project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(project) + + for credential_type in ['cert', 'ec2', 'totp']: + + if credential_type == 'ec2': + blob_value = ("{\"access\":\"" + uuid.uuid4().hex + + "\",\"secret\":\"secretKey\"}") + else: + blob_value = uuid.uuid4().hex + + credential = self.client.credentials.create(user=user.id, + type=credential_type, + blob=blob_value, + project=project.id) + self.client.credentials.delete(credential.id) + self.assertRaises(http.NotFound, + self.client.credentials.get, + credential.id) diff --git a/keystoneclient/tests/functional/v3/test_domain_configs.py b/keystoneclient/tests/functional/v3/test_domain_configs.py new file mode 100644 index 000000000..f3ca71a22 --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_domain_configs.py @@ -0,0 +1,106 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class DomainConfigsTestCase(base.V3ClientTestCase): + + def setUp(self): + super(DomainConfigsTestCase, self).setUp() + self.test_domain = fixtures.Domain(self.client) + self.useFixture(self.test_domain) + + def check_domain_config(self, config, config_ref): + for attr in config_ref: + self.assertEqual( + getattr(config, attr), + config_ref[attr], + 'Expected different %s' % attr) + + def _new_ref(self): + return {'identity': {'driver': uuid.uuid4().hex}, + 'ldap': {'url': uuid.uuid4().hex}} + + def test_create_domain_config(self): + config_ref = self._new_ref() + config = self.client.domain_configs.create( + self.test_domain.id, config_ref) + self.addCleanup( + self.client.domain_configs.delete, self.test_domain.id) + self.check_domain_config(config, config_ref) + + def test_create_invalid_domain_config(self): + invalid_groups_ref = { + uuid.uuid4().hex: {uuid.uuid4().hex: uuid.uuid4().hex}, + uuid.uuid4().hex: {uuid.uuid4().hex: uuid.uuid4().hex}} + self.assertRaises(http.Forbidden, + self.client.domain_configs.create, + self.test_domain.id, + invalid_groups_ref) + + invalid_options_ref = { + 'identity': {uuid.uuid4().hex: uuid.uuid4().hex}, + 'ldap': {uuid.uuid4().hex: uuid.uuid4().hex}} + self.assertRaises(http.Forbidden, + self.client.domain_configs.create, + self.test_domain.id, + invalid_options_ref) + + def test_get_domain_config(self): + config = fixtures.DomainConfig(self.client, self.test_domain.id) + self.useFixture(config) + + config_ret = self.client.domain_configs.get(self.test_domain.id) + self.check_domain_config(config_ret, config.ref) + + def test_update_domain_config(self): + config = fixtures.DomainConfig(self.client, self.test_domain.id) + self.useFixture(config) + + update_config_ref = self._new_ref() + config_ret = self.client.domain_configs.update( + self.test_domain.id, update_config_ref) + self.check_domain_config(config_ret, update_config_ref) + + def test_update_invalid_domain_config(self): + config = fixtures.DomainConfig(self.client, self.test_domain.id) + self.useFixture(config) + + invalid_groups_ref = { + uuid.uuid4().hex: {uuid.uuid4().hex: uuid.uuid4().hex}, + uuid.uuid4().hex: {uuid.uuid4().hex: uuid.uuid4().hex}} + self.assertRaises(http.Forbidden, + self.client.domain_configs.update, + self.test_domain.id, + invalid_groups_ref) + + invalid_options_ref = { + 'identity': {uuid.uuid4().hex: uuid.uuid4().hex}, + 'ldap': {uuid.uuid4().hex: uuid.uuid4().hex}} + self.assertRaises(http.Forbidden, + self.client.domain_configs.update, + self.test_domain.id, + invalid_options_ref) + + def test_domain_config_delete(self): + config_ref = self._new_ref() + self.client.domain_configs.create(self.test_domain.id, config_ref) + + self.client.domain_configs.delete(self.test_domain.id) + self.assertRaises(http.NotFound, + self.client.domain_configs.get, + self.project_domain_id) diff --git a/keystoneclient/tests/functional/v3/test_domains.py b/keystoneclient/tests/functional/v3/test_domains.py new file mode 100644 index 000000000..cdfbdd742 --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_domains.py @@ -0,0 +1,97 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class DomainsTestCase(base.V3ClientTestCase): + + def check_domain(self, domain, domain_ref=None): + self.assertIsNotNone(domain.id) + self.assertIn('self', domain.links) + self.assertIn('/domains/' + domain.id, domain.links['self']) + + if domain_ref: + self.assertEqual(domain_ref['name'], domain.name) + self.assertEqual(domain_ref['enabled'], domain.enabled) + + # There is no guarantee description is present in domain + if hasattr(domain_ref, 'description'): + self.assertEqual(domain_ref['description'], domain.description) + else: + # Only check remaining mandatory attributes + self.assertIsNotNone(domain.name) + self.assertIsNotNone(domain.enabled) + + def test_create_domain(self): + domain_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True} + domain = self.client.domains.create(**domain_ref) + self.check_domain(domain, domain_ref) + + # Only disabled domains can be deleted + self.addCleanup(self.client.domains.delete, domain) + self.addCleanup(self.client.domains.update, domain, enabled=False) + + def test_get_domain(self): + domain_id = self.project_domain_id + domain_ret = self.client.domains.get(domain_id) + self.check_domain(domain_ret) + + def test_list_domains(self): + domain_one = fixtures.Domain(self.client) + self.useFixture(domain_one) + + domain_two = fixtures.Domain(self.client) + self.useFixture(domain_two) + + domains = self.client.domains.list() + + # All domains are valid + for domain in domains: + self.check_domain(domain) + + self.assertIn(domain_one.entity, domains) + self.assertIn(domain_two.entity, domains) + + def test_update_domain(self): + domain = fixtures.Domain(self.client) + self.useFixture(domain) + + new_description = uuid.uuid4().hex + domain_ret = self.client.domains.update(domain.id, + description=new_description) + + domain.ref.update({'description': new_description}) + self.check_domain(domain_ret, domain.ref) + + def test_delete_domain(self): + domain = self.client.domains.create(name=uuid.uuid4().hex, + description=uuid.uuid4().hex, + enabled=True) + + # Only disabled domains can be deleted + self.assertRaises(http.Forbidden, + self.client.domains.delete, + domain.id) + + self.client.domains.update(domain, enabled=False) + self.client.domains.delete(domain.id) + self.assertRaises(http.NotFound, + self.client.domains.get, + domain.id) diff --git a/keystoneclient/tests/functional/v3/test_ec2.py b/keystoneclient/tests/functional/v3/test_ec2.py new file mode 100644 index 000000000..09703b0db --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_ec2.py @@ -0,0 +1,92 @@ +# 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 http + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class EC2TestCase(base.V3ClientTestCase): + + def check_ec2(self, ec2, ec2_ref=None): + self.assertIn('self', ec2.links) + self.assertIn('/users/%s/credentials/OS-EC2/%s' + % (ec2.user_id, ec2.access), ec2.links['self']) + + if ec2_ref: + self.assertEqual(ec2_ref['user_id'], ec2.user_id) + self.assertEqual(ec2_ref['project_id'], ec2.tenant_id) + + else: + self.assertIsNotNone(ec2.user_id) + self.assertIsNotNone(ec2.tenant_id) + + def test_create_ec2(self): + user = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user) + project = fixtures.Project(self.client, self.project_domain_id) + self.useFixture(project) + + ec2_ref = {'user_id': user.id, + 'project_id': project.id} + ec2 = self.client.ec2.create(**ec2_ref) + + self.addCleanup(self.client.ec2.delete, user.id, ec2.access) + self.check_ec2(ec2, ec2_ref) + + def test_get_ec2(self): + user = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user) + project = fixtures.Project(self.client, self.project_domain_id) + self.useFixture(project) + + ec2 = fixtures.EC2(self.client, user_id=user.id, project_id=project.id) + self.useFixture(ec2) + + ec2_ret = self.client.ec2.get(user.id, ec2.access) + self.check_ec2(ec2_ret, ec2.ref) + + def test_list_ec2(self): + user_one = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user_one) + ec2_one = fixtures.EC2(self.client, user_id=user_one.id, + project_id=self.project_domain_id) + self.useFixture(ec2_one) + + user_two = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user_two) + ec2_two = fixtures.EC2(self.client, user_id=user_two.id, + project_id=self.project_domain_id) + self.useFixture(ec2_two) + + ec2_list = self.client.ec2.list(user_one.id) + + # All ec2 are valid + for ec2 in ec2_list: + self.check_ec2(ec2) + + self.assertIn(ec2_one.entity, ec2_list) + self.assertNotIn(ec2_two.entity, ec2_list) + + def test_delete_ec2(self): + user = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user) + project = fixtures.Project(self.client, self.project_domain_id) + self.useFixture(project) + + ec2 = self.client.ec2.create(user.id, project.id) + + self.client.ec2.delete(user.id, ec2.access) + self.assertRaises(http.NotFound, + self.client.ec2.get, + user.id, ec2.access) diff --git a/keystoneclient/tests/functional/v3/test_endpoint_filters.py b/keystoneclient/tests/functional/v3/test_endpoint_filters.py new file mode 100644 index 000000000..d8956bed2 --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_endpoint_filters.py @@ -0,0 +1,86 @@ +# 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 http + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures +from keystoneclient.tests.functional.v3 import test_endpoint_groups +from keystoneclient.tests.functional.v3 import test_projects + + +class EndpointFiltersTestCase(base.V3ClientTestCase, + test_endpoint_groups.EndpointGroupsTestMixin, + test_projects.ProjectsTestMixin): + + def setUp(self): + super(EndpointFiltersTestCase, self).setUp() + + self.project = fixtures.Project(self.client) + self.endpoint_group = fixtures.EndpointGroup(self.client) + self.useFixture(self.project) + self.useFixture(self.endpoint_group) + + self.client.endpoint_filter.add_endpoint_group_to_project( + self.endpoint_group, self.project) + + def test_add_endpoint_group_to_project(self): + project = fixtures.Project(self.client) + endpoint_group = fixtures.EndpointGroup(self.client) + self.useFixture(project) + self.useFixture(endpoint_group) + + self.client.endpoint_filter.add_endpoint_group_to_project( + endpoint_group, project) + self.client.endpoint_filter.check_endpoint_group_in_project( + endpoint_group, project) + + def test_delete_endpoint_group_from_project(self): + self.client.endpoint_filter.delete_endpoint_group_from_project( + self.endpoint_group, self.project) + self.assertRaises( + http.NotFound, + self.client.endpoint_filter.check_endpoint_group_in_project, + self.endpoint_group, self.project) + + def test_list_endpoint_groups_for_project(self): + endpoint_group_two = fixtures.EndpointGroup(self.client) + self.useFixture(endpoint_group_two) + self.client.endpoint_filter.add_endpoint_group_to_project( + endpoint_group_two, self.project) + + endpoint_groups = ( + self.client.endpoint_filter.list_endpoint_groups_for_project( + self.project + ) + ) + + for endpoint_group in endpoint_groups: + self.check_endpoint_group(endpoint_group) + + self.assertIn(self.endpoint_group.entity, endpoint_groups) + self.assertIn(endpoint_group_two.entity, endpoint_groups) + + def test_list_projects_for_endpoint_group(self): + project_two = fixtures.Project(self.client) + self.useFixture(project_two) + self.client.endpoint_filter.add_endpoint_group_to_project( + self.endpoint_group, project_two) + + f = self.client.endpoint_filter.list_projects_for_endpoint_group + projects = f(self.endpoint_group) + + for project in projects: + self.check_project(project) + + self.assertIn(self.project.entity, projects) + self.assertIn(project_two.entity, projects) diff --git a/keystoneclient/tests/functional/v3/test_endpoint_groups.py b/keystoneclient/tests/functional/v3/test_endpoint_groups.py new file mode 100644 index 000000000..52fcf724e --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_endpoint_groups.py @@ -0,0 +1,123 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class EndpointGroupsTestMixin(object): + + def check_endpoint_group(self, endpoint_group, endpoint_group_ref=None): + self.assertIsNotNone(endpoint_group.id) + self.assertIn('self', endpoint_group.links) + self.assertIn('/endpoint_groups/' + endpoint_group.id, + endpoint_group.links['self']) + + if endpoint_group_ref: + self.assertEqual(endpoint_group_ref['name'], endpoint_group.name) + self.assertEqual(endpoint_group_ref['filters'], + endpoint_group.filters) + + # There is no guarantee description is present in endpoint groups + if hasattr(endpoint_group_ref, 'description'): + self.assertEqual(endpoint_group_ref['description'], + endpoint_group.description) + else: + # Only check remaining mandatory attributes + self.assertIsNotNone(endpoint_group.name) + self.assertIsNotNone(endpoint_group.filters) + + +class EndpointGroupsTestCase(base.V3ClientTestCase, EndpointGroupsTestMixin): + + def test_create_endpoint_group(self): + endpoint_group_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'filters': {'interface': 'internal'}, + 'description': uuid.uuid4().hex} + endpoint_group = self.client.endpoint_groups.create( + **endpoint_group_ref) + + self.addCleanup(self.client.endpoint_groups.delete, endpoint_group) + self.check_endpoint_group(endpoint_group, endpoint_group_ref) + + def test_get_endpoint_group(self): + endpoint_group = fixtures.EndpointGroup(self.client) + self.useFixture(endpoint_group) + + endpoint_ret = self.client.endpoint_groups.get(endpoint_group.id) + self.check_endpoint_group(endpoint_ret, endpoint_group.ref) + + self.assertRaises(http.NotFound, + self.client.endpoint_groups.get, + uuid.uuid4().hex) + + def test_check_endpoint_group(self): + endpoint_group = fixtures.EndpointGroup(self.client) + self.useFixture(endpoint_group) + + self.client.endpoint_groups.check(endpoint_group.id) + self.assertRaises(http.NotFound, + self.client.endpoint_groups.check, + uuid.uuid4().hex) + + def test_list_endpoint_groups(self): + endpoint_group_one = fixtures.EndpointGroup(self.client) + self.useFixture(endpoint_group_one) + + endpoint_group_two = fixtures.EndpointGroup(self.client) + self.useFixture(endpoint_group_two) + + endpoint_groups = self.client.endpoint_groups.list() + + # All endpoints are valid + for endpoint_group in endpoint_groups: + self.check_endpoint_group(endpoint_group) + + self.assertIn(endpoint_group_one.entity, endpoint_groups) + self.assertIn(endpoint_group_two.entity, endpoint_groups) + + def test_update_endpoint_group(self): + endpoint_group = fixtures.EndpointGroup(self.client) + self.useFixture(endpoint_group) + + new_name = fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex + new_filters = {'interface': 'public'} + new_description = uuid.uuid4().hex + + endpoint_group_ret = self.client.endpoint_groups.update( + endpoint_group, + name=new_name, + filters=new_filters, + description=new_description) + + endpoint_group.ref.update({'name': new_name, 'filters': new_filters, + 'description': new_description}) + self.check_endpoint_group(endpoint_group_ret, endpoint_group.ref) + + def test_delete_endpoint_group(self): + endpoint_group = self.client.endpoint_groups.create( + name=fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + filters={'interface': 'admin'}, + description=uuid.uuid4().hex) + + self.client.endpoint_groups.delete(endpoint_group.id) + self.assertRaises(http.NotFound, + self.client.endpoint_groups.check, + endpoint_group.id) + self.assertRaises(http.NotFound, + self.client.endpoint_groups.get, + endpoint_group.id) diff --git a/keystoneclient/tests/functional/v3/test_endpoints.py b/keystoneclient/tests/functional/v3/test_endpoints.py new file mode 100644 index 000000000..c83797064 --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_endpoints.py @@ -0,0 +1,141 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class EndpointsTestCase(base.V3ClientTestCase): + + def check_endpoint(self, endpoint, endpoint_ref=None): + self.assertIsNotNone(endpoint.id) + self.assertIn('self', endpoint.links) + self.assertIn('/endpoints/' + endpoint.id, endpoint.links['self']) + + if endpoint_ref: + self.assertEqual(endpoint_ref['service'], endpoint.service_id) + self.assertEqual(endpoint_ref['url'], endpoint.url) + self.assertEqual(endpoint_ref['interface'], endpoint.interface) + self.assertEqual(endpoint_ref['enabled'], endpoint.enabled) + + # There is no guarantee below attributes are present in endpoint + if hasattr(endpoint_ref, 'region'): + self.assertEqual(endpoint_ref['region'], endpoint.region) + + else: + # Only check remaining mandatory attributes + self.assertIsNotNone(endpoint.service_id) + self.assertIsNotNone(endpoint.url) + self.assertIsNotNone(endpoint.interface) + self.assertIsNotNone(endpoint.enabled) + + def test_create_endpoint(self): + service = fixtures.Service(self.client) + self.useFixture(service) + + endpoint_ref = {'service': service.id, + 'url': 'http://' + uuid.uuid4().hex, + 'enabled': True, + 'interface': 'public'} + endpoint = self.client.endpoints.create(**endpoint_ref) + + self.addCleanup(self.client.endpoints.delete, endpoint) + self.check_endpoint(endpoint, endpoint_ref) + + def test_get_endpoint(self): + service = fixtures.Service(self.client) + self.useFixture(service) + + interfaces = ['public', 'admin', 'internal'] + for interface in interfaces: + endpoint = fixtures.Endpoint(self.client, service.id, interface) + self.useFixture(endpoint) + endpoint_ret = self.client.endpoints.get(endpoint.id) + # All endpoints are valid + self.check_endpoint(endpoint_ret, endpoint.ref) + + def test_list_endpoints(self): + service = fixtures.Service(self.client) + self.useFixture(service) + + region = fixtures.Region(self.client) + self.useFixture(region) + + endpoint_one = fixtures.Endpoint(self.client, service.id, 'public', + region=region.id) + self.useFixture(endpoint_one) + + endpoint_two = fixtures.Endpoint(self.client, service.id, 'admin', + region=region.id) + self.useFixture(endpoint_two) + + endpoint_three = fixtures.Endpoint(self.client, service.id, 'internal', + region=region.id) + self.useFixture(endpoint_three) + + endpoints = self.client.endpoints.list() + + # All endpoints are valid + for endpoint in endpoints: + self.check_endpoint(endpoint) + + self.assertIn(endpoint_one.entity, endpoints) + self.assertIn(endpoint_two.entity, endpoints) + self.assertIn(endpoint_three.entity, endpoints) + + def test_update_endpoint(self): + service = fixtures.Service(self.client) + self.useFixture(service) + + new_service = fixtures.Service(self.client) + self.useFixture(new_service) + + new_region = fixtures.Region(self.client) + self.useFixture(new_region) + + endpoint = fixtures.Endpoint(self.client, service.id, 'public') + self.useFixture(endpoint) + + new_url = 'http://' + uuid.uuid4().hex + new_interface = 'internal' + new_enabled = False + + endpoint_ret = self.client.endpoints.update(endpoint.id, + service=new_service.id, + url=new_url, + interface=new_interface, + enabled=new_enabled, + region=new_region.id) + + endpoint.ref.update({'service': new_service.id, 'url': new_url, + 'interface': new_interface, + 'enabled': new_enabled, + 'region': new_region.entity}) + self.check_endpoint(endpoint_ret, endpoint.ref) + + def test_delete_endpoint(self): + service = fixtures.Service(self.client) + self.useFixture(service) + endpoint = self.client.endpoints.create(service=service.id, + url='http://' + + uuid.uuid4().hex, + enabled=True, + interface='public') + + self.client.endpoints.delete(endpoint.id) + self.assertRaises(http.NotFound, + self.client.endpoints.get, + endpoint.id) diff --git a/keystoneclient/tests/functional/v3/test_federation.py b/keystoneclient/tests/functional/v3/test_federation.py new file mode 100644 index 000000000..da312662b --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_federation.py @@ -0,0 +1,106 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http + +from keystoneclient.tests.functional import base + + +class TestIdentityProviders(base.V3ClientTestCase): + + def test_idp_create(self): + idp_id = uuid.uuid4().hex + # Create an identity provider just passing an ID + idp = self.client.federation.identity_providers.create(id=idp_id) + self.addCleanup( + self.client.federation.identity_providers.delete, idp_id) + + self.assertEqual(idp_id, idp.id) + self.assertEqual([], idp.remote_ids) + self.assertFalse(idp.enabled) + + def test_idp_create_enabled_true(self): + # Create an enabled identity provider + idp_id = uuid.uuid4().hex + idp = self.client.federation.identity_providers.create( + id=idp_id, enabled=True) + self.addCleanup( + self.client.federation.identity_providers.delete, idp_id) + + self.assertEqual(idp_id, idp.id) + self.assertEqual([], idp.remote_ids) + self.assertTrue(idp.enabled) + + def test_idp_create_with_remote_ids(self): + # Create an enabled identity provider with remote IDs + idp_id = uuid.uuid4().hex + remote_ids = [uuid.uuid4().hex, uuid.uuid4().hex] + idp = self.client.federation.identity_providers.create( + id=idp_id, enabled=True, remote_ids=remote_ids) + self.addCleanup( + self.client.federation.identity_providers.delete, idp_id) + + self.assertEqual(idp_id, idp.id) + self.assertIn(remote_ids[0], idp.remote_ids) + self.assertIn(remote_ids[1], idp.remote_ids) + self.assertTrue(idp.enabled) + + def test_idp_list(self): + idp_ids = [] + for _ in range(3): + idp_id = uuid.uuid4().hex + self.client.federation.identity_providers.create(id=idp_id) + self.addCleanup( + self.client.federation.identity_providers.delete, idp_id) + idp_ids.append(idp_id) + + idp_list = self.client.federation.identity_providers.list() + fetched_ids = [fetched_idp.id for fetched_idp in idp_list] + for idp_id in idp_ids: + self.assertIn(idp_id, fetched_ids) + + def test_idp_get(self): + idp_id = uuid.uuid4().hex + remote_ids = [uuid.uuid4().hex, uuid.uuid4().hex] + idp_create = self.client.federation.identity_providers.create( + id=idp_id, enabled=True, remote_ids=remote_ids) + self.addCleanup( + self.client.federation.identity_providers.delete, idp_id) + + idp_get = self.client.federation.identity_providers.get(idp_id) + self.assertEqual(idp_create.id, idp_get.id) + self.assertEqual(idp_create.enabled, idp_get.enabled) + self.assertIn(idp_create.remote_ids[0], idp_get.remote_ids) + self.assertIn(idp_create.remote_ids[1], idp_get.remote_ids) + + def test_idp_delete(self): + idp_id = uuid.uuid4().hex + self.client.federation.identity_providers.create(id=idp_id) + + # Get should not raise an error + self.client.federation.identity_providers.get(idp_id) + + # Delete it + self.client.federation.identity_providers.delete(idp_id) + + # Now get should raise an error + self.assertRaises( + http.NotFound, + self.client.federation.identity_providers.get, + idp_id) + + # Should not be present in the identity provider list + idp_list = self.client.federation.identity_providers.list() + fetched_ids = [fetched_idp.id for fetched_idp in idp_list] + self.assertNotIn(idp_id, fetched_ids) diff --git a/keystoneclient/tests/functional/v3/test_groups.py b/keystoneclient/tests/functional/v3/test_groups.py new file mode 100644 index 000000000..5e818ab19 --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_groups.py @@ -0,0 +1,94 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class GroupsTestCase(base.V3ClientTestCase): + + def check_group(self, group, group_ref=None): + self.assertIsNotNone(group.id) + self.assertIn('self', group.links) + self.assertIn('/groups/' + group.id, group.links['self']) + + if group_ref: + self.assertEqual(group_ref['name'], group.name) + self.assertEqual(group_ref['domain'], group.domain_id) + + # There is no guarantee description is present in group + if hasattr(group_ref, 'description'): + self.assertEqual(group_ref['description'], group.description) + + else: + # Only check remaining mandatory attributes + self.assertIsNotNone(group.name) + self.assertIsNotNone(group.domain_id) + + def test_create_group(self): + group_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.project_domain_id, + 'description': uuid.uuid4().hex} + + group = self.client.groups.create(**group_ref) + self.addCleanup(self.client.groups.delete, group) + self.check_group(group, group_ref) + + def test_get_group(self): + group = fixtures.Group(self.client, self.project_domain_id) + self.useFixture(group) + + group_ret = self.client.groups.get(group.id) + self.check_group(group_ret, group.ref) + + def test_list_groups(self): + group_one = fixtures.Group(self.client, self.project_domain_id) + self.useFixture(group_one) + + group_two = fixtures.Group(self.client, self.project_domain_id) + self.useFixture(group_two) + + groups = self.client.groups.list() + + # All groups are valid + for group in groups: + self.check_group(group) + + self.assertIn(group_one.entity, groups) + self.assertIn(group_two.entity, groups) + + def test_update_group(self): + group = fixtures.Group(self.client, self.project_domain_id) + self.useFixture(group) + + new_name = fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex + new_description = uuid.uuid4().hex + + group_ret = self.client.groups.update(group.id, + name=new_name, + description=new_description) + + group.ref.update({'name': new_name, 'description': new_description}) + self.check_group(group_ret, group.ref) + + def test_delete_group(self): + group = self.client.groups.create(name=uuid.uuid4().hex, + domain=self.project_domain_id) + + self.client.groups.delete(group.id) + self.assertRaises(http.NotFound, + self.client.groups.get, + group.id) diff --git a/keystoneclient/tests/functional/v3/test_implied_roles.py b/keystoneclient/tests/functional/v3/test_implied_roles.py new file mode 100644 index 000000000..4a8b446ea --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_implied_roles.py @@ -0,0 +1,70 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +role_defs = ["test_admin", + "test_id_manager", + "test_resource_manager", + "test_role_manager", + "test_assignment_manager", + "test_domain_manager", + "test_project_manager", + "test_catalog_manager", + "test_policy_manager", + "test_observer", + "test_domain_tech_lead", + "test_project_observer", + "test_member"] + +inference_rules = {"test_admin": "test_policy_manager", + "test_id_manager": "test_project_observer", + "test_resource_manager": "test_project_observer", + "test_role_manager": "test_project_observer", + "test_catalog_manager": "test_project_observer", + "test_policy_manager": "test_project_observer", + "test_project_observer": "test_observer", + "test_member": "test_observer"} + + +class TestImpliedRoles(base.V3ClientTestCase): + + def setUp(self): + super(TestImpliedRoles, self).setUp() + + def test_implied_roles(self): + initial_rule_count = ( + len(self.client.inference_rules.list_inference_roles())) + + self.create_roles() + self.create_rules() + rule_count = len(self.client.inference_rules.list_inference_roles()) + self.assertEqual(initial_rule_count + len(inference_rules), + rule_count) + + def role_dict(self): + roles = {role.name: role.id for role in self.client.roles.list()} + return roles + + def create_roles(self): + for role_def in role_defs: + role = fixtures.Role(self.client, name=role_def) + self.useFixture(role) + + def create_rules(self): + roles = self.role_dict() + for prior, implied in inference_rules.items(): + rule = fixtures.InferenceRule(self.client, roles[prior], + roles[implied]) + self.useFixture(rule) diff --git a/keystoneclient/tests/functional/v3/test_policies.py b/keystoneclient/tests/functional/v3/test_policies.py new file mode 100644 index 000000000..6ad0aae73 --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_policies.py @@ -0,0 +1,89 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class PoliciesTestCase(base.V3ClientTestCase): + + def check_policy(self, policy, policy_ref=None): + self.assertIsNotNone(policy.id) + self.assertIn('self', policy.links) + self.assertIn('/policies/' + policy.id, policy.links['self']) + + if policy_ref: + self.assertEqual(policy_ref['blob'], policy.blob) + self.assertEqual(policy_ref['type'], policy.type) + + else: + # Only check remaining mandatory attributes + self.assertIsNotNone(policy.blob) + self.assertIsNotNone(policy.type) + + def test_create_policy(self): + policy_ref = {'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex} + + policy = self.client.policies.create(**policy_ref) + self.addCleanup(self.client.policies.delete, policy) + self.check_policy(policy, policy_ref) + + def test_get_policy(self): + policy = fixtures.Policy(self.client) + self.useFixture(policy) + + policy_ret = self.client.policies.get(policy.id) + self.check_policy(policy_ret, policy.ref) + + def test_list_policies(self): + policy_one = fixtures.Policy(self.client) + self.useFixture(policy_one) + + policy_two = fixtures.Policy(self.client) + self.useFixture(policy_two) + + policies = self.client.policies.list() + + # All policies are valid + for policy in policies: + self.check_policy(policy) + + self.assertIn(policy_one.entity, policies) + self.assertIn(policy_two.entity, policies) + + def test_update_policy(self): + policy = fixtures.Policy(self.client) + self.useFixture(policy) + + new_blob = uuid.uuid4().hex + new_type = uuid.uuid4().hex + + policy_ret = self.client.policies.update(policy.id, + blob=new_blob, + type=new_type) + + policy.ref.update({'blob': new_blob, 'type': new_type}) + self.check_policy(policy_ret, policy.ref) + + def test_delete_policy(self): + policy = self.client.policies.create(blob=uuid.uuid4().hex, + type=uuid.uuid4().hex) + + self.client.policies.delete(policy.id) + self.assertRaises(http.NotFound, + self.client.policies.get, + policy.id) diff --git a/keystoneclient/tests/functional/v3/test_projects.py b/keystoneclient/tests/functional/v3/test_projects.py new file mode 100644 index 000000000..a7f082db1 --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_projects.py @@ -0,0 +1,469 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http +from keystoneclient import exceptions +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class ProjectsTestMixin(object): + + def check_project(self, project, project_ref=None): + self.assertIsNotNone(project.id) + self.assertIn('self', project.links) + self.assertIn('/projects/' + project.id, project.links['self']) + + if project_ref: + self.assertEqual(project_ref['name'], project.name) + self.assertEqual(project_ref['domain'], project.domain_id) + self.assertEqual(project_ref['enabled'], project.enabled) + + # There is no guarantee the attributes below are present in project + if hasattr(project_ref, 'description'): + self.assertEqual(project_ref['description'], + project.description) + if hasattr(project_ref, 'parent'): + self.assertEqual(project_ref['parent'], project.parent) + + else: + # Only check remaining mandatory attributes + self.assertIsNotNone(project.name) + self.assertIsNotNone(project.domain_id) + self.assertIsNotNone(project.enabled) + + +class ProjectsTestCase(base.V3ClientTestCase, ProjectsTestMixin): + + def setUp(self): + super(ProjectsTestCase, self).setUp() + self.test_domain = fixtures.Domain(self.client) + self.useFixture(self.test_domain) + + self.test_project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(self.test_project) + self.special_tag = '~`!@#$%^&*()-_+=<>.? \'"' + + def test_create_subproject(self): + project_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.test_domain.id, + 'enabled': True, + 'description': uuid.uuid4().hex, + 'parent': self.test_project.id} + + project = self.client.projects.create(**project_ref) + self.addCleanup(self.client.projects.delete, project) + self.check_project(project, project_ref) + + def test_create_project(self): + project_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.test_domain.id, + 'enabled': True, + 'description': uuid.uuid4().hex} + + project = self.client.projects.create(**project_ref) + self.addCleanup(self.client.projects.delete, project) + self.check_project(project, project_ref) + + def test_get_project(self): + project_ret = self.client.projects.get(self.test_project.id) + self.check_project(project_ret, self.test_project.ref) + + def test_get_project_invalid_params(self): + self.assertRaises(exceptions.ValidationError, + self.client.projects.get, + self.test_project.id, + subtree_as_list=True, subtree_as_ids=True) + self.assertRaises(exceptions.ValidationError, + self.client.projects.get, + self.test_project.id, + parents_as_list=True, parents_as_ids=True) + + def test_get_hierarchy_as_list(self): + project = fixtures.Project(self.client, self.test_domain.id, + parent=self.test_project.id) + self.useFixture(project) + + child_project = fixtures.Project(self.client, self.test_domain.id, + parent=project.id) + self.useFixture(child_project) + + # Only parents and subprojects that the current user has role + # assingments on are returned when asked for subtree_as_list and + # parents_as_list. + role = fixtures.Role(self.client) + self.useFixture(role) + self.client.roles.grant(role.id, user=self.user_id, + project=self.test_project.id) + self.client.roles.grant(role.id, user=self.user_id, + project=project.id) + self.client.roles.grant(role.id, user=self.user_id, + project=child_project.id) + + project_ret = self.client.projects.get(project.id, + subtree_as_list=True, + parents_as_list=True) + + self.check_project(project_ret, project.ref) + self.assertCountEqual( + [{'project': self.test_project.entity.to_dict()}], + project_ret.parents) + self.assertCountEqual( + [{'project': child_project.entity.to_dict()}], + project_ret.subtree) + + def test_get_hierarchy_as_ids(self): + project = fixtures.Project(self.client, self.test_domain.id, + parent=self.test_project.id) + self.useFixture(project) + + child_project = fixtures.Project(self.client, self.test_domain.id, + parent=project.id) + self.useFixture(child_project) + + project_ret = self.client.projects.get(project.id, + subtree_as_ids=True, + parents_as_ids=True) + + self.assertCountEqual([self.test_project.id], project_ret.parents) + self.assertCountEqual([child_project.id], project_ret.subtree) + + def test_list_projects(self): + project_one = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(project_one) + + project_two = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(project_two) + + projects = self.client.projects.list() + + # All projects are valid + for project in projects: + self.check_project(project) + + self.assertIn(project_one.entity, projects) + self.assertIn(project_two.entity, projects) + + def test_list_subprojects(self): + parent_project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(parent_project) + + child_project_one = fixtures.Project(self.client, self.test_domain.id, + parent=parent_project.id) + self.useFixture(child_project_one) + + child_project_two = fixtures.Project(self.client, self.test_domain.id, + parent=parent_project.id) + self.useFixture(child_project_two) + + projects = self.client.projects.list(parent=parent_project.id) + + # All projects are valid + for project in projects: + self.check_project(project) + + self.assertIn(child_project_one.entity, projects) + self.assertIn(child_project_two.entity, projects) + + # Parent project should not be included in the result + self.assertNotIn(parent_project.entity, projects) + + def test_update_project(self): + project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(project) + + new_name = fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex + new_description = uuid.uuid4().hex + project_ret = self.client.projects.update(project.id, + name=new_name, + enabled=False, + description=new_description) + + project.ref.update({'name': new_name, 'enabled': False, + 'description': new_description}) + self.check_project(project_ret, project.ref) + + def test_update_project_domain_not_allowed(self): + domain = fixtures.Domain(self.client) + self.useFixture(domain) + # Cannot update domain after project is created. + self.assertRaises(http.BadRequest, + self.client.projects.update, + self.test_project.id, domain=domain.id) + + def test_delete_project(self): + project = self.client.projects.create(name=uuid.uuid4().hex, + domain=self.test_domain.id, + enabled=True) + + self.client.projects.delete(project.id) + self.assertRaises(http.NotFound, + self.client.projects.get, + project.id) + + def test_list_projects_with_tag_filters(self): + project_one = fixtures.Project( + self.client, self.test_domain.id, + tags=['tag1']) + project_two = fixtures.Project( + self.client, self.test_domain.id, + tags=['tag1', 'tag2']) + project_three = fixtures.Project( + self.client, self.test_domain.id, + tags=['tag2', 'tag3']) + + self.useFixture(project_one) + self.useFixture(project_two) + self.useFixture(project_three) + + projects = self.client.projects.list(tags='tag1') + project_ids = [] + for project in projects: + project_ids.append(project.id) + self.assertIn(project_one.id, project_ids) + + projects = self.client.projects.list(tags_any='tag1') + project_ids = [] + for project in projects: + project_ids.append(project.id) + self.assertIn(project_one.id, project_ids) + self.assertIn(project_two.id, project_ids) + + projects = self.client.projects.list(not_tags='tag1') + project_ids = [] + for project in projects: + project_ids.append(project.id) + self.assertNotIn(project_one.id, project_ids) + + projects = self.client.projects.list(not_tags_any='tag1,tag2') + project_ids = [] + for project in projects: + project_ids.append(project.id) + self.assertNotIn(project_one.id, project_ids) + self.assertNotIn(project_two.id, project_ids) + self.assertNotIn(project_three.id, project_ids) + + projects = self.client.projects.list(tags='tag1,tag2') + project_ids = [] + for project in projects: + project_ids.append(project.id) + self.assertNotIn(project_one.id, project_ids) + self.assertIn(project_two.id, project_ids) + self.assertNotIn(project_three.id, project_ids) + + def test_add_tag(self): + project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(project) + + tags = self.client.projects.get(project.id).tags + self.assertEqual([], tags) + + project.add_tag('tag1') + tags = self.client.projects.get(project.id).tags + self.assertEqual(['tag1'], tags) + + # verify there is an error when you try to add the same tag + self.assertRaises(http.BadRequest, + project.add_tag, + 'tag1') + + def test_update_tags(self): + project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(project) + + tags = self.client.projects.get(project.id).tags + self.assertEqual([], tags) + + project.update_tags(['tag1', 'tag2', self.special_tag]) + tags = self.client.projects.get(project.id).tags + self.assertIn('tag1', tags) + self.assertIn('tag2', tags) + self.assertIn(self.special_tag, tags) + self.assertEqual(3, len(tags)) + + project.update_tags([]) + tags = self.client.projects.get(project.id).tags + self.assertEqual([], tags) + + # cannot have duplicate tags in update + self.assertRaises(http.BadRequest, + project.update_tags, + ['tag1', 'tag1']) + + def test_delete_tag(self): + project = fixtures.Project( + self.client, self.test_domain.id, + tags=['tag1', self.special_tag]) + self.useFixture(project) + + project.delete_tag('tag1') + tags = self.client.projects.get(project.id).tags + self.assertEqual([self.special_tag], tags) + + project.delete_tag(self.special_tag) + tags = self.client.projects.get(project.id).tags + self.assertEqual([], tags) + + def test_delete_all_tags(self): + project_one = fixtures.Project( + self.client, self.test_domain.id, + tags=['tag1']) + + project_two = fixtures.Project( + self.client, self.test_domain.id, + tags=['tag1', 'tag2', self.special_tag]) + + project_three = fixtures.Project( + self.client, self.test_domain.id, + tags=[]) + + self.useFixture(project_one) + self.useFixture(project_two) + self.useFixture(project_three) + + result_one = project_one.delete_all_tags() + tags_one = self.client.projects.get(project_one.id).tags + tags_two = self.client.projects.get(project_two.id).tags + self.assertEqual([], result_one) + self.assertEqual([], tags_one) + self.assertIn('tag1', tags_two) + + result_two = project_two.delete_all_tags() + tags_two = self.client.projects.get(project_two.id).tags + self.assertEqual([], result_two) + self.assertEqual([], tags_two) + + result_three = project_three.delete_all_tags() + tags_three = self.client.projects.get(project_three.id).tags + self.assertEqual([], result_three) + self.assertEqual([], tags_three) + + def test_list_tags(self): + tags_one = ['tag1'] + project_one = fixtures.Project( + self.client, self.test_domain.id, + tags=tags_one) + + tags_two = ['tag1', 'tag2'] + project_two = fixtures.Project( + self.client, self.test_domain.id, + tags=tags_two) + + tags_three = [] + project_three = fixtures.Project( + self.client, self.test_domain.id, + tags=tags_three) + + self.useFixture(project_one) + self.useFixture(project_two) + self.useFixture(project_three) + + result_one = project_one.list_tags() + result_two = project_two.list_tags() + result_three = project_three.list_tags() + + for tag in tags_one: + self.assertIn(tag, result_one) + self.assertEqual(1, len(result_one)) + + for tag in tags_two: + self.assertIn(tag, result_two) + self.assertEqual(2, len(result_two)) + + for tag in tags_three: + self.assertIn(tag, result_three) + self.assertEqual(0, len(result_three)) + + def test_check_tag(self): + project = fixtures.Project( + self.client, self.test_domain.id, + tags=['tag1']) + self.useFixture(project) + + tags = self.client.projects.get(project.id).tags + self.assertEqual(['tag1'], tags) + self.assertTrue(project.check_tag('tag1')) + self.assertFalse(project.check_tag('tag2')) + self.assertFalse(project.check_tag(self.special_tag)) + + def test_add_invalid_tags(self): + project_one = fixtures.Project( + self.client, self.test_domain.id) + + self.useFixture(project_one) + + self.assertRaises(exceptions.BadRequest, + project_one.add_tag, + ',') + self.assertRaises(exceptions.BadRequest, + project_one.add_tag, + '/') + self.assertRaises(exceptions.BadRequest, + project_one.add_tag, + '') + + def test_update_invalid_tags(self): + tags_comma = ['tag1', ','] + tags_slash = ['tag1', '/'] + tags_blank = ['tag1', ''] + project_one = fixtures.Project( + self.client, self.test_domain.id) + + self.useFixture(project_one) + + self.assertRaises(exceptions.BadRequest, + project_one.update_tags, + tags_comma) + self.assertRaises(exceptions.BadRequest, + project_one.update_tags, + tags_slash) + self.assertRaises(exceptions.BadRequest, + project_one.update_tags, + tags_blank) + + def test_create_project_invalid_tags(self): + project_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.test_domain.id, + 'enabled': True, + 'description': uuid.uuid4().hex, + 'tags': ','} + + self.assertRaises(exceptions.BadRequest, + self.client.projects.create, + **project_ref) + + project_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.test_domain.id, + 'enabled': True, + 'description': uuid.uuid4().hex, + 'tags': '/'} + + self.assertRaises(exceptions.BadRequest, + self.client.projects.create, + **project_ref) + + project_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.test_domain.id, + 'enabled': True, + 'description': uuid.uuid4().hex, + 'tags': ''} + + self.assertRaises(exceptions.BadRequest, + self.client.projects.create, + **project_ref) diff --git a/keystoneclient/tests/functional/v3/test_regions.py b/keystoneclient/tests/functional/v3/test_regions.py new file mode 100644 index 000000000..51f93863e --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_regions.py @@ -0,0 +1,85 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class RegionsTestCase(base.V3ClientTestCase): + + def check_region(self, region, region_ref=None): + self.assertIsNotNone(region.id) + self.assertIn('self', region.links) + self.assertIn('/regions/' + region.id, region.links['self']) + + # There is no guarantee the below attributes are present in region + if hasattr(region_ref, 'description'): + self.assertEqual(region_ref['description'], region.description) + if hasattr(region_ref, 'parent_region'): + self.assertEqual( + region_ref['parent_region'], + region.parent_region) + + def test_create_region(self): + region_ref = {'description': uuid.uuid4().hex} + + region = self.client.regions.create(**region_ref) + self.addCleanup(self.client.regions.delete, region) + self.check_region(region, region_ref) + + def test_get_region(self): + region = fixtures.Region(self.client) + self.useFixture(region) + + region_ret = self.client.regions.get(region.id) + self.check_region(region_ret, region.ref) + + def test_list_regions(self): + region_one = fixtures.Region(self.client) + self.useFixture(region_one) + + region_two = fixtures.Region(self.client, parent_region=region_one.id) + self.useFixture(region_two) + + regions = self.client.regions.list() + + # All regions are valid + for region in regions: + self.check_region(region) + + self.assertIn(region_one.entity, regions) + self.assertIn(region_two.entity, regions) + + def test_update_region(self): + parent = fixtures.Region(self.client) + self.useFixture(parent) + + region = fixtures.Region(self.client) + self.useFixture(region) + + new_description = uuid.uuid4().hex + region_ret = self.client.regions.update(region.id, + description=new_description, + parent_region=parent.id) + self.check_region(region_ret, region.ref) + + def test_delete_region(self): + region = self.client.regions.create() + + self.client.regions.delete(region.id) + self.assertRaises(http.NotFound, + self.client.regions.get, + region.id) diff --git a/keystoneclient/tests/functional/v3/test_roles.py b/keystoneclient/tests/functional/v3/test_roles.py new file mode 100644 index 000000000..6dba6ffa3 --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_roles.py @@ -0,0 +1,236 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http +from keystoneclient import exceptions +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class RolesTestCase(base.V3ClientTestCase): + + def check_role(self, role, role_ref=None): + self.assertIsNotNone(role.id) + self.assertIn('self', role.links) + self.assertIn('/roles/' + role.id, role.links['self']) + + if role_ref: + self.assertEqual(role_ref['name'], role.name) + + # There is no guarantee domain is present in role + if hasattr(role_ref, 'domain'): + self.assertEqual(role_ref['domain'], role.domain_id) + + else: + # Only check remaining mandatory attribute + self.assertIsNotNone(role.name) + + def test_create_role(self): + role_ref = {'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex} + + role = self.client.roles.create(**role_ref) + self.addCleanup(self.client.roles.delete, role) + self.check_role(role, role_ref) + + def test_create_domain_role(self): + role_ref = {'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.project_domain_id} + + role = self.client.roles.create(**role_ref) + self.addCleanup(self.client.roles.delete, role) + self.check_role(role, role_ref) + + def test_get_role(self): + role = fixtures.Role(self.client, domain=self.project_domain_id) + self.useFixture(role) + + role_ret = self.client.roles.get(role.id) + self.check_role(role_ret, role.ref) + + def test_update_role_name(self): + role = fixtures.Role(self.client, domain=self.project_domain_id) + self.useFixture(role) + + new_name = fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex + role_ret = self.client.roles.update(role.id, + name=new_name) + + role.ref.update({'name': new_name}) + self.check_role(role_ret, role.ref) + + def test_update_role_domain(self): + role = fixtures.Role(self.client) + self.useFixture(role) + + domain = fixtures.Domain(self.client) + self.useFixture(domain) + new_domain = domain.id + role_ret = self.client.roles.update(role.id, + domain=new_domain) + + role.ref.update({'domain': new_domain}) + self.check_role(role_ret, role.ref) + + def test_list_roles_invalid_params(self): + user = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user) + + # Only filter in role grants for a user on a resource. + # Domain or project should be specified. + self.assertRaises(exceptions.ValidationError, + self.client.roles.list, + user=user.id) + + # Only filter in role grants for a group on a resource. + # Domain or project should be specified. + group = fixtures.Group(self.client, self.project_domain_id) + self.useFixture(group) + + self.assertRaises(exceptions.ValidationError, + self.client.roles.list, + group=group.id) + + def test_list_roles(self): + global_role = fixtures.Role(self.client) + self.useFixture(global_role) + + domain = fixtures.Domain(self.client) + self.useFixture(domain) + + domain_role = fixtures.Role(self.client, domain=domain.id) + self.useFixture(domain_role) + + global_roles = self.client.roles.list() + domain_roles = self.client.roles.list(domain_id=domain.id) + roles = global_roles + domain_roles + + # All roles are valid + for role in roles: + self.check_role(role) + + self.assertIn(global_role.entity, global_roles) + self.assertIn(domain_role.entity, domain_roles) + + def test_delete_role(self): + role = self.client.roles.create(name=uuid.uuid4().hex, + domain=self.project_domain_id) + + self.client.roles.delete(role.id) + self.assertRaises(http.NotFound, + self.client.roles.get, + role.id) + + def test_grant_role_invalid_params(self): + user = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user) + + role = fixtures.Role(self.client, domain=self.project_domain_id) + self.useFixture(role) + + # Only grant role to a group on a resource. + # Domain or project must be specified. + self.assertRaises(exceptions.ValidationError, + self.client.roles.grant, + role.id, + user=user.id) + + group = fixtures.Group(self.client, self.project_domain_id) + self.useFixture(group) + + # Only grant role to a group on a resource. + # Domain or project must be specified. + self.assertRaises(exceptions.ValidationError, + self.client.roles.grant, + role.id, + group=group.id) + + def test_user_domain_grant_and_revoke(self): + user = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user) + + domain = fixtures.Domain(self.client) + self.useFixture(domain) + + role = fixtures.Role(self.client, domain=self.project_domain_id) + self.useFixture(role) + + self.client.roles.grant(role, user=user.id, domain=domain.id) + roles_after_grant = self.client.roles.list(user=user.id, + domain=domain.id) + self.assertCountEqual(roles_after_grant, [role.entity]) + + self.client.roles.revoke(role, user=user.id, domain=domain.id) + roles_after_revoke = self.client.roles.list(user=user.id, + domain=domain.id) + self.assertEqual(roles_after_revoke, []) + + def test_user_project_grant_and_revoke(self): + user = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user) + + project = fixtures.Project(self.client, self.project_domain_id) + self.useFixture(project) + + role = fixtures.Role(self.client, domain=self.project_domain_id) + self.useFixture(role) + + self.client.roles.grant(role, user=user.id, project=project.id) + roles_after_grant = self.client.roles.list(user=user.id, + project=project.id) + self.assertCountEqual(roles_after_grant, [role.entity]) + + self.client.roles.revoke(role, user=user.id, project=project.id) + roles_after_revoke = self.client.roles.list(user=user.id, + project=project.id) + self.assertEqual(roles_after_revoke, []) + + def test_group_domain_grant_and_revoke(self): + group = fixtures.Group(self.client, self.project_domain_id) + self.useFixture(group) + + domain = fixtures.Domain(self.client) + self.useFixture(domain) + + role = fixtures.Role(self.client, domain=self.project_domain_id) + self.useFixture(role) + + self.client.roles.grant(role, group=group.id, domain=domain.id) + roles_after_grant = self.client.roles.list(group=group.id, + domain=domain.id) + self.assertCountEqual(roles_after_grant, [role.entity]) + + self.client.roles.revoke(role, group=group.id, domain=domain.id) + roles_after_revoke = self.client.roles.list(group=group.id, + domain=domain.id) + self.assertEqual(roles_after_revoke, []) + + def test_group_project_grant_and_revoke(self): + group = fixtures.Group(self.client, self.project_domain_id) + self.useFixture(group) + + project = fixtures.Project(self.client, self.project_domain_id) + self.useFixture(project) + + role = fixtures.Role(self.client, domain=self.project_domain_id) + self.useFixture(role) + + self.client.roles.grant(role, group=group.id, project=project.id) + roles_after_grant = self.client.roles.list(group=group.id, + project=project.id) + self.assertCountEqual(roles_after_grant, [role.entity]) + + self.client.roles.revoke(role, group=group.id, project=project.id) + roles_after_revoke = self.client.roles.list(group=group.id, + project=project.id) + self.assertEqual(roles_after_revoke, []) diff --git a/keystoneclient/tests/functional/v3/test_services.py b/keystoneclient/tests/functional/v3/test_services.py new file mode 100644 index 000000000..c17747d3e --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_services.py @@ -0,0 +1,106 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class ServicesTestCase(base.V3ClientTestCase): + + def check_service(self, service, service_ref=None): + self.assertIsNotNone(service.id) + self.assertIn('self', service.links) + self.assertIn('/services/' + service.id, service.links['self']) + + if service_ref: + self.assertEqual(service_ref['name'], service.name) + self.assertEqual(service_ref['enabled'], service.enabled) + self.assertEqual(service_ref['type'], service.type) + + # There is no guarantee description is present in service + if hasattr(service_ref, 'description'): + self.assertEqual(service_ref['description'], + service.description) + + else: + # Only check remaining mandatory attributes + self.assertIsNotNone(service.name) + self.assertIsNotNone(service.enabled) + self.assertIsNotNone(service.type) + + def test_create_service(self): + service_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'enabled': True, + 'description': uuid.uuid4().hex} + + service = self.client.services.create(**service_ref) + + self.addCleanup(self.client.services.delete, service) + self.check_service(service, service_ref) + + def test_get_service(self): + service = fixtures.Service(self.client) + self.useFixture(service) + + service_ret = self.client.services.get(service.id) + self.check_service(service_ret, service.ref) + + def test_list_services(self): + service_one = fixtures.Service(self.client) + self.useFixture(service_one) + + service_two = fixtures.Service(self.client) + self.useFixture(service_two) + + services = self.client.services.list() + + # All services are valid + for service in services: + self.check_service(service) + + self.assertIn(service_one.entity, services) + self.assertIn(service_two.entity, services) + + def test_update_service(self): + service = fixtures.Service(self.client) + self.useFixture(service) + + new_name = fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex + new_type = uuid.uuid4().hex + new_enabled = False + new_description = uuid.uuid4().hex + + service_ret = self.client.services.update(service.id, + name=new_name, + type=new_type, + enabled=new_enabled, + description=new_description) + + service.ref.update({'name': new_name, 'type': new_type, + 'enabled': new_enabled, + 'description': new_description}) + self.check_service(service_ret, service.ref) + + def test_delete_service(self): + service = self.client.services.create(name=uuid.uuid4().hex, + type=uuid.uuid4().hex) + + self.client.services.delete(service.id) + self.assertRaises(http.NotFound, + self.client.services.get, + service.id) diff --git a/keystoneclient/tests/functional/v3/test_users.py b/keystoneclient/tests/functional/v3/test_users.py new file mode 100644 index 000000000..780ddbacf --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_users.py @@ -0,0 +1,140 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1.exceptions import http +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures + + +class UsersTestCase(base.V3ClientTestCase): + + def check_user(self, user, user_ref=None): + self.assertIsNotNone(user.id) + self.assertIsNotNone(user.enabled) + self.assertIn('self', user.links) + self.assertIn('/users/' + user.id, user.links['self']) + + if user_ref: + self.assertEqual(user_ref['name'], user.name) + self.assertEqual(user_ref['domain'], user.domain_id) + # There is no guarantee the attributes below are present in user + if hasattr(user_ref, 'description'): + self.assertEqual(user_ref['description'], user.description) + if hasattr(user_ref, 'email'): + self.assertEqual(user_ref['email'], user.email) + if hasattr(user_ref, 'default_project'): + self.assertEqual(user_ref['default_project'], + user.default_project_id) + else: + # Only check remaining mandatory attributes + self.assertIsNotNone(user.name) + self.assertIsNotNone(user.domain_id) + + def test_create_user(self): + user_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.project_domain_id, + 'default_project': self.project_id, + 'password': uuid.uuid4().hex, + 'description': uuid.uuid4().hex} + + user = self.client.users.create(**user_ref) + self.addCleanup(self.client.users.delete, user) + self.check_user(user, user_ref) + + def test_get_user(self): + user = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user) + + user_ret = self.client.users.get(user.id) + self.check_user(user_ret, user.ref) + + def test_list_users(self): + user_one = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user_one) + + user_two = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user_two) + + users = self.client.users.list() + + # All users are valid + for user in users: + self.check_user(user) + + self.assertIn(user_one.entity, users) + self.assertIn(user_two.entity, users) + + def test_list_users_with_filters(self): + suffix = uuid.uuid4().hex + user1_ref = { + 'name': 'test_user' + suffix, + 'domain': self.project_domain_id, + 'default_project': self.project_id, + 'password': uuid.uuid4().hex, + 'description': uuid.uuid4().hex} + + user2_ref = { + 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, + 'domain': self.project_domain_id, + 'default_project': self.project_id, + 'password': uuid.uuid4().hex, + 'description': uuid.uuid4().hex} + + user1 = self.client.users.create(**user1_ref) + self.client.users.create(**user2_ref) + + users = self.client.users.list(name__contains=['test_user', suffix]) + self.assertEqual(1, len(users)) + self.assertIn(user1, users) + + def test_update_user(self): + user = fixtures.User(self.client, self.project_domain_id) + self.useFixture(user) + + new_description = uuid.uuid4().hex + user_ret = self.client.users.update(user.id, + description=new_description) + + user.ref.update({'description': new_description}) + self.check_user(user_ret, user.ref) + + def test_user_grouping(self): + # keystoneclient.v3.users owns user grouping operations, this is why + # this test case belongs to this class + user = fixtures.User(self.client, self.project_domain_id) + group = fixtures.Group(self.client, self.project_domain_id) + self.useFixture(user) + self.useFixture(group) + + self.assertRaises(http.NotFound, + self.client.users.check_in_group, + user.id, group.id) + + self.client.users.add_to_group(user.id, group.id) + self.client.users.check_in_group(user.id, group.id) + self.client.users.remove_from_group(user.id, group.id) + + self.assertRaises(http.NotFound, + self.client.users.check_in_group, + user.id, group.id) + + def test_delete_user(self): + user = self.client.users.create(name=uuid.uuid4().hex, + domain=self.project_domain_id) + + self.client.users.delete(user.id) + self.assertRaises(http.NotFound, + self.client.users.get, + user.id) diff --git a/tests/v2_0/__init__.py b/keystoneclient/tests/unit/__init__.py similarity index 100% rename from tests/v2_0/__init__.py rename to keystoneclient/tests/unit/__init__.py diff --git a/tests/v3/__init__.py b/keystoneclient/tests/unit/apiclient/__init__.py similarity index 100% rename from tests/v3/__init__.py rename to keystoneclient/tests/unit/apiclient/__init__.py diff --git a/keystoneclient/tests/unit/apiclient/test_exceptions.py b/keystoneclient/tests/unit/apiclient/test_exceptions.py new file mode 100644 index 000000000..65cf08016 --- /dev/null +++ b/keystoneclient/tests/unit/apiclient/test_exceptions.py @@ -0,0 +1,66 @@ +# Copyright 2012 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. + +from keystoneclient import exceptions +from keystoneclient.tests.unit import utils + + +class FakeResponse(object): + json_data = {} + + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + def json(self): + return self.json_data + + +class ExceptionsArgsTest(utils.TestCase): + + def assert_exception(self, ex_cls, method, url, status_code, json_data): + ex = exceptions.from_response( + FakeResponse(status_code=status_code, + headers={"Content-Type": "application/json"}, + json_data=json_data), + method, + url) + self.assertIsInstance(ex, ex_cls) + self.assertIn(json_data["error"]["message"], ex.message) + self.assertEqual(ex.details, json_data["error"]["details"]) + self.assertEqual(ex.method, method) + self.assertEqual(ex.url, url) + self.assertEqual(ex.http_status, status_code) + + def test_from_response_known(self): + method = "GET" + url = "/fake" + status_code = 400 + json_data = {"error": {"message": "fake message", + "details": "fake details"}} + self.assert_exception( + exceptions.BadRequest, method, url, status_code, json_data) + + def test_from_response_unknown(self): + method = "POST" + url = "/fake-unknown" + status_code = 499 + json_data = {"error": {"message": "fake unknown message", + "details": "fake unknown details"}} + self.assert_exception( + exceptions.HTTPClientError, method, url, status_code, json_data) + status_code = 600 + self.assert_exception( + exceptions.HTTPError, method, url, status_code, json_data) diff --git a/keystoneclient/tests/unit/auth/__init__.py b/keystoneclient/tests/unit/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/keystoneclient/tests/unit/auth/test_access.py b/keystoneclient/tests/unit/auth/test_access.py new file mode 100644 index 000000000..f5f5843a8 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_access.py @@ -0,0 +1,64 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1 import fixture +from keystoneauth1 import plugin + +from keystoneclient import access +from keystoneclient.auth.identity import access as access_plugin +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class AccessInfoPluginTests(utils.TestCase): + + def setUp(self): + super(AccessInfoPluginTests, self).setUp() + with self.deprecations.expect_deprecations_here(): + self.session = session.Session() + self.auth_token = uuid.uuid4().hex + + def _plugin(self, **kwargs): + token = fixture.V3Token() + s = token.add_service('identity') + s.add_standard_endpoints(public=self.TEST_ROOT_URL) + + auth_ref = access.AccessInfo.factory(body=token, + auth_token=self.auth_token) + with self.deprecations.expect_deprecations_here(): + return access_plugin.AccessInfoPlugin(auth_ref, **kwargs) + + def test_auth_ref(self): + plugin = self._plugin() + self.assertEqual(self.TEST_ROOT_URL, + plugin.get_endpoint(self.session, + service_type='identity', + interface='public')) + self.assertEqual(self.auth_token, plugin.get_token(session)) + + def test_auth_url(self): + auth_url = 'http://keystone.test.url' + plug = self._plugin(auth_url=auth_url) + + self.assertEqual(auth_url, + plug.get_endpoint(self.session, + interface=plugin.AUTH_INTERFACE)) + + def test_invalidate(self): + plugin = self._plugin() + auth_ref = plugin.auth_ref + + self.assertIsInstance(auth_ref, access.AccessInfo) + self.assertFalse(plugin.invalidate()) + self.assertIs(auth_ref, plugin.auth_ref) diff --git a/keystoneclient/tests/unit/auth/test_auth.py b/keystoneclient/tests/unit/auth/test_auth.py new file mode 100644 index 000000000..3a56fb2aa --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_auth.py @@ -0,0 +1,22 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.tests.unit.auth import utils + + +class AuthTests(utils.TestCase): + + def test_plugin_names_in_available(self): + pass + + def test_plugin_classes_in_available(self): + pass diff --git a/keystoneclient/tests/unit/auth/test_cli.py b/keystoneclient/tests/unit/auth/test_cli.py new file mode 100644 index 000000000..b2a2f6ae8 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_cli.py @@ -0,0 +1,197 @@ +# 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 +from unittest import mock +import uuid + +import fixtures +from oslo_config import cfg + +from keystoneclient.auth import base +from keystoneclient.auth import cli +from keystoneclient.tests.unit.auth import utils + + +class TesterPlugin(base.BaseAuthPlugin): + + def get_token(self, *args, **kwargs): + return None + + @classmethod + def get_options(cls): + # NOTE(jamielennox): this is kind of horrible. If you specify this as + # a deprecated_name= value it will convert - to _ which is not what we + # want for a CLI option. + deprecated = [cfg.DeprecatedOpt('test-other')] + return [ + cfg.StrOpt('test-opt', help='tester', deprecated_opts=deprecated) + ] + + +class CliTests(utils.TestCase): + + def setUp(self): + super(CliTests, self).setUp() + self.deprecations.expect_deprecations() + self.p = argparse.ArgumentParser() + + def env(self, name, value=None): + if value is not None: + # environment variables are always strings + value = str(value) + + return self.useFixture(fixtures.EnvironmentVariable(name, value)) + + def test_creating_with_no_args(self): + ret = cli.register_argparse_arguments(self.p, []) + self.assertIsNone(ret) + self.assertIn('--os-auth-plugin', self.p.format_usage()) + + def test_load_with_nothing(self): + cli.register_argparse_arguments(self.p, []) + opts = self.p.parse_args([]) + self.assertIsNone(cli.load_from_argparse_arguments(opts)) + + @utils.mock_plugin + def test_basic_params_added(self, m): + name = uuid.uuid4().hex + argv = ['--os-auth-plugin', name] + ret = cli.register_argparse_arguments(self.p, argv) + self.assertIs(utils.MockPlugin, ret) + + for n in ('--os-a-int', '--os-a-bool', '--os-a-float'): + self.assertIn(n, self.p.format_usage()) + + m.assert_called_once_with(name) + + @utils.mock_plugin + def test_param_loading(self, m): + name = uuid.uuid4().hex + argv = ['--os-auth-plugin', name, + '--os-a-int', str(self.a_int), + '--os-a-float', str(self.a_float), + '--os-a-bool', str(self.a_bool)] + + klass = cli.register_argparse_arguments(self.p, argv) + self.assertIs(utils.MockPlugin, klass) + + opts = self.p.parse_args(argv) + self.assertEqual(name, opts.os_auth_plugin) + + a = cli.load_from_argparse_arguments(opts) + self.assertTestVals(a) + + self.assertEqual(name, opts.os_auth_plugin) + self.assertEqual(str(self.a_int), opts.os_a_int) + self.assertEqual(str(self.a_float), opts.os_a_float) + self.assertEqual(str(self.a_bool), opts.os_a_bool) + + @utils.mock_plugin + def test_default_options(self, m): + name = uuid.uuid4().hex + argv = ['--os-auth-plugin', name, + '--os-a-float', str(self.a_float)] + + klass = cli.register_argparse_arguments(self.p, argv) + self.assertIs(utils.MockPlugin, klass) + + opts = self.p.parse_args(argv) + self.assertEqual(name, opts.os_auth_plugin) + + a = cli.load_from_argparse_arguments(opts) + + self.assertEqual(self.a_float, a['a_float']) + self.assertEqual(3, a['a_int']) + + @utils.mock_plugin + def test_with_default_string_value(self, m): + name = uuid.uuid4().hex + klass = cli.register_argparse_arguments(self.p, [], default=name) + self.assertIs(utils.MockPlugin, klass) + m.assert_called_once_with(name) + + @utils.mock_plugin + def test_overrides_default_string_value(self, m): + name = uuid.uuid4().hex + default = uuid.uuid4().hex + argv = ['--os-auth-plugin', name] + klass = cli.register_argparse_arguments(self.p, argv, default=default) + self.assertIs(utils.MockPlugin, klass) + m.assert_called_once_with(name) + + @utils.mock_plugin + def test_with_default_type_value(self, m): + klass = cli.register_argparse_arguments(self.p, [], + default=utils.MockPlugin) + self.assertIs(utils.MockPlugin, klass) + self.assertEqual(0, m.call_count) + + @utils.mock_plugin + def test_overrides_default_type_value(self, m): + # using this test plugin would fail if called because there + # is no get_options() function + class TestPlugin(object): + pass + name = uuid.uuid4().hex + argv = ['--os-auth-plugin', name] + klass = cli.register_argparse_arguments(self.p, argv, + default=TestPlugin) + self.assertIs(utils.MockPlugin, klass) + m.assert_called_once_with(name) + + @utils.mock_plugin + def test_env_overrides_default_opt(self, m): + name = uuid.uuid4().hex + val = uuid.uuid4().hex + self.env('OS_A_STR', val) + + klass = cli.register_argparse_arguments(self.p, [], default=name) + opts = self.p.parse_args([]) + a = klass.load_from_argparse_arguments(opts) + + self.assertEqual(val, a['a_str']) + + def test_deprecated_cli_options(self): + TesterPlugin.register_argparse_arguments(self.p) + val = uuid.uuid4().hex + opts = self.p.parse_args(['--os-test-other', val]) + self.assertEqual(val, opts.os_test_opt) + + def test_deprecated_multi_cli_options(self): + TesterPlugin.register_argparse_arguments(self.p) + val1 = uuid.uuid4().hex + val2 = uuid.uuid4().hex + # argarse rules say that the last specified wins. + opts = self.p.parse_args(['--os-test-other', val2, + '--os-test-opt', val1]) + self.assertEqual(val1, opts.os_test_opt) + + def test_deprecated_env_options(self): + val = uuid.uuid4().hex + + with mock.patch.dict('os.environ', {'OS_TEST_OTHER': val}): + TesterPlugin.register_argparse_arguments(self.p) + + opts = self.p.parse_args([]) + self.assertEqual(val, opts.os_test_opt) + + def test_deprecated_env_multi_options(self): + val1 = uuid.uuid4().hex + val2 = uuid.uuid4().hex + + with mock.patch.dict('os.environ', {'OS_TEST_OPT': val1, + 'OS_TEST_OTHER': val2}): + TesterPlugin.register_argparse_arguments(self.p) + + opts = self.p.parse_args([]) + self.assertEqual(val1, opts.os_test_opt) diff --git a/keystoneclient/tests/unit/auth/test_conf.py b/keystoneclient/tests/unit/auth/test_conf.py new file mode 100644 index 000000000..bea37a4f1 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_conf.py @@ -0,0 +1,116 @@ +# 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 unittest import mock +import uuid + +from oslo_config import cfg +from oslo_config import fixture as config + +from keystoneclient.auth import base +from keystoneclient.auth import conf +from keystoneclient import exceptions +from keystoneclient.tests.unit.auth import utils + + +class ConfTests(utils.TestCase): + + def setUp(self): + super(ConfTests, self).setUp() + self.deprecations.expect_deprecations() + self.conf_fixture = self.useFixture(config.Config()) + + # NOTE(jamielennox): we register the basic config options first because + # we need them in place before we can stub them. We will need to run + # the register again after we stub the auth section and auth plugin so + # it can load the plugin specific options. + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + def test_loading_v2(self): + pass + + def test_loading_v3(self): + pass + + def test_loading_invalid_plugin(self): + auth_plugin = uuid.uuid4().hex + self.conf_fixture.config(auth_plugin=auth_plugin, + group=self.GROUP) + + e = self.assertRaises(exceptions.NoMatchingPlugin, + conf.load_from_conf_options, + self.conf_fixture.conf, + self.GROUP) + + self.assertEqual(auth_plugin, e.name) + + def test_loading_with_no_data(self): + self.assertIsNone(conf.load_from_conf_options(self.conf_fixture.conf, + self.GROUP)) + + @mock.patch('stevedore.DriverManager') + def test_other_params(self, m): + m.return_value = utils.MockManager(utils.MockPlugin) + driver_name = uuid.uuid4().hex + + self.conf_fixture.register_opts(utils.MockPlugin.get_options(), + group=self.GROUP) + self.conf_fixture.config(auth_plugin=driver_name, + group=self.GROUP, + **self.TEST_VALS) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertTestVals(a) + + m.assert_called_once_with(namespace=base.PLUGIN_NAMESPACE, + name=driver_name, + invoke_on_load=False) + + @utils.mock_plugin + def test_same_section(self, m): + self.conf_fixture.register_opts(utils.MockPlugin.get_options(), + group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + self.conf_fixture.config(auth_plugin=uuid.uuid4().hex, + group=self.GROUP, + **self.TEST_VALS) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertTestVals(a) + + @utils.mock_plugin + def test_diff_section(self, m): + section = uuid.uuid4().hex + + self.conf_fixture.config(auth_section=section, group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.conf_fixture.register_opts(utils.MockPlugin.get_options(), + group=section) + self.conf_fixture.config(group=section, + auth_plugin=uuid.uuid4().hex, + **self.TEST_VALS) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertTestVals(a) + + def test_plugins_are_all_opts(self): + pass + + def test_get_common(self): + opts = conf.get_common_conf_options() + for opt in opts: + self.assertIsInstance(opt, cfg.Opt) + self.assertEqual(2, len(opts)) + + def test_get_named(self): + pass diff --git a/keystoneclient/tests/unit/auth/test_default_cli.py b/keystoneclient/tests/unit/auth/test_default_cli.py new file mode 100644 index 000000000..8afd2cde7 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_default_cli.py @@ -0,0 +1,92 @@ +# 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 +from unittest import mock +import uuid + + +from keystoneclient.auth.identity.generic import cli +from keystoneclient import exceptions +from keystoneclient.tests.unit import utils + + +class DefaultCliTests(utils.TestCase): + + def setUp(self): + super(DefaultCliTests, self).setUp() + self.deprecations.expect_deprecations() + + def new_plugin(self, argv): + parser = argparse.ArgumentParser() + cli.DefaultCLI.register_argparse_arguments(parser) + opts = parser.parse_args(argv) + return cli.DefaultCLI.load_from_argparse_arguments(opts) + + def test_endpoint_override(self): + password = uuid.uuid4().hex + url = uuid.uuid4().hex + + p = self.new_plugin(['--os-auth-url', 'url', + '--os-endpoint', url, + '--os-password', password]) + + self.assertEqual(url, p.get_endpoint(None)) + self.assertEqual(password, p._password) + + def test_token_only_override(self): + self.assertRaises(exceptions.CommandError, + self.new_plugin, + ['--os-token', uuid.uuid4().hex]) + + def test_token_endpoint_override(self): + token = uuid.uuid4().hex + endpoint = uuid.uuid4().hex + + p = self.new_plugin(['--os-endpoint', endpoint, + '--os-token', token]) + + self.assertEqual(endpoint, p.get_endpoint(None)) + self.assertEqual(token, p.get_token(None)) + + def test_no_auth_url(self): + exc = self.assertRaises(exceptions.CommandError, + self.new_plugin, + ['--os-username', uuid.uuid4().hex]) + + self.assertIn('auth-url', str(exc)) + + @mock.patch('sys.stdin', autospec=True) + @mock.patch('getpass.getpass') + def test_prompt_password(self, mock_getpass, mock_stdin): + password = uuid.uuid4().hex + + mock_stdin.isatty = lambda: True + mock_getpass.return_value = password + + p = self.new_plugin(['--os-auth-url', uuid.uuid4().hex, + '--os-username', uuid.uuid4().hex]) + + self.assertEqual(password, p._password) + + @mock.patch('sys.stdin', autospec=True) + @mock.patch('getpass.getpass') + def test_prompt_no_password(self, mock_getpass, mock_stdin): + mock_stdin.isatty = lambda: True + mock_getpass.return_value = '' + + exc = self.assertRaises(exceptions.CommandError, + self.new_plugin, + ['--os-auth-url', uuid.uuid4().hex, + '--os-username', uuid.uuid4().hex]) + + self.assertIn('password', str(exc)) diff --git a/keystoneclient/tests/unit/auth/test_identity_common.py b/keystoneclient/tests/unit/auth/test_identity_common.py new file mode 100644 index 000000000..3e8cc2b81 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_identity_common.py @@ -0,0 +1,504 @@ +# 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 datetime +from unittest import mock +import uuid + +from keystoneauth1 import fixture +from keystoneauth1 import plugin +from oslo_utils import timeutils + +from keystoneclient import access +from keystoneclient.auth import base +from keystoneclient.auth import identity +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class CommonIdentityTests(object, metaclass=abc.ABCMeta): + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + + TEST_COMPUTE_PUBLIC = 'http://nova/novapi/public' + TEST_COMPUTE_INTERNAL = 'http://nova/novapi/internal' + TEST_COMPUTE_ADMIN = 'http://nova/novapi/admin' + + TEST_PASS = uuid.uuid4().hex + + def setUp(self): + super(CommonIdentityTests, self).setUp() + self.deprecations.expect_deprecations() + + self.TEST_URL = '%s%s' % (self.TEST_ROOT_URL, self.version) + self.TEST_ADMIN_URL = '%s%s' % (self.TEST_ROOT_ADMIN_URL, self.version) + self.TEST_DISCOVERY = fixture.DiscoveryList(href=self.TEST_ROOT_URL) + + self.stub_auth_data() + + @abc.abstractmethod + def create_auth_plugin(self, **kwargs): + """Create an auth plugin that makes sense for the auth data. + + It doesn't really matter what auth mechanism is used but it should be + appropriate to the API version. + """ + pass + + @abc.abstractmethod + def get_auth_data(self, **kwargs): + """Return fake authentication data. + + This should register a valid token response and ensure that the compute + endpoints are set to TEST_COMPUTE_PUBLIC, _INTERNAL and _ADMIN. + """ + pass + + def stub_auth_data(self, **kwargs): + token = self.get_auth_data(**kwargs) + self.user_id = token.user_id + + try: + self.project_id = token.project_id + except AttributeError: + self.project_id = token.tenant_id + + self.stub_auth(json=token) + + @property + @abc.abstractmethod + def version(self): + """The API version being tested.""" + + def test_discovering(self): + self.stub_url('GET', [], + base_url=self.TEST_COMPUTE_ADMIN, + json=self.TEST_DISCOVERY) + + body = 'SUCCESS' + + # which gives our sample values + self.stub_url('GET', ['path'], text=body) + + a = self.create_auth_plugin() + s = session.Session(auth=a) + + resp = s.get('/path', endpoint_filter={'service_type': 'compute', + 'interface': 'admin', + 'version': self.version}) + + self.assertEqual(200, resp.status_code) + self.assertEqual(body, resp.text) + + new_body = 'SC SUCCESS' + # if we don't specify a version, we use the URL from the SC + self.stub_url('GET', ['path'], + base_url=self.TEST_COMPUTE_ADMIN, + text=new_body) + + resp = s.get('/path', endpoint_filter={'service_type': 'compute', + 'interface': 'admin'}) + + self.assertEqual(200, resp.status_code) + self.assertEqual(new_body, resp.text) + + def test_discovery_uses_session_cache(self): + # register responses such that if the discovery URL is hit more than + # once then the response will be invalid and not point to COMPUTE_ADMIN + resps = [{'json': self.TEST_DISCOVERY}, {'status_code': 500}] + self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) + + body = 'SUCCESS' + self.stub_url('GET', ['path'], text=body) + + # now either of the two plugins I use, it should not cause a second + # request to the discovery url. + s = session.Session() + a = self.create_auth_plugin() + b = self.create_auth_plugin() + + for auth in (a, b): + resp = s.get('/path', + auth=auth, + endpoint_filter={'service_type': 'compute', + 'interface': 'admin', + 'version': self.version}) + + self.assertEqual(200, resp.status_code) + self.assertEqual(body, resp.text) + + def test_discovery_uses_plugin_cache(self): + # register responses such that if the discovery URL is hit more than + # once then the response will be invalid and not point to COMPUTE_ADMIN + resps = [{'json': self.TEST_DISCOVERY}, {'status_code': 500}] + self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) + + body = 'SUCCESS' + self.stub_url('GET', ['path'], text=body) + + # now either of the two sessions I use, it should not cause a second + # request to the discovery url. + sa = session.Session() + sb = session.Session() + auth = self.create_auth_plugin() + + for sess in (sa, sb): + resp = sess.get('/path', + auth=auth, + endpoint_filter={'service_type': 'compute', + 'interface': 'admin', + 'version': self.version}) + + self.assertEqual(200, resp.status_code) + self.assertEqual(body, resp.text) + + def test_discovering_with_no_data(self): + # which returns discovery information pointing to TEST_URL but there is + # no data there. + self.stub_url('GET', [], + base_url=self.TEST_COMPUTE_ADMIN, + status_code=400) + + # so the url that will be used is the same TEST_COMPUTE_ADMIN + body = 'SUCCESS' + self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN, + text=body, status_code=200) + + a = self.create_auth_plugin() + s = session.Session(auth=a) + + resp = s.get('/path', endpoint_filter={'service_type': 'compute', + 'interface': 'admin', + 'version': self.version}) + + self.assertEqual(200, resp.status_code) + self.assertEqual(body, resp.text) + + def test_asking_for_auth_endpoint_ignores_checks(self): + a = self.create_auth_plugin() + s = session.Session(auth=a) + + auth_url = s.get_endpoint(service_type='compute', + interface=plugin.AUTH_INTERFACE) + + self.assertEqual(self.TEST_URL, auth_url) + + def _create_expired_auth_plugin(self, **kwargs): + expires = timeutils.utcnow() - datetime.timedelta(minutes=20) + expired_token = self.get_auth_data(expires=expires) + expired_auth_ref = access.AccessInfo.factory(body=expired_token) + + body = 'SUCCESS' + self.stub_url('GET', ['path'], + base_url=self.TEST_COMPUTE_ADMIN, text=body) + + a = self.create_auth_plugin(**kwargs) + a.auth_ref = expired_auth_ref + return a + + def test_reauthenticate(self): + a = self._create_expired_auth_plugin() + expired_auth_ref = a.auth_ref + s = session.Session(auth=a) + self.assertIsNot(expired_auth_ref, a.get_access(s)) + + def test_no_reauthenticate(self): + a = self._create_expired_auth_plugin(reauthenticate=False) + expired_auth_ref = a.auth_ref + s = session.Session(auth=a) + self.assertIs(expired_auth_ref, a.get_access(s)) + + def test_invalidate(self): + a = self.create_auth_plugin() + s = session.Session(auth=a) + + # trigger token fetching + s.get_auth_headers() + + self.assertTrue(a.auth_ref) + self.assertTrue(a.invalidate()) + self.assertIsNone(a.auth_ref) + self.assertFalse(a.invalidate()) + + def test_get_auth_properties(self): + a = self.create_auth_plugin() + s = session.Session() + + self.assertEqual(self.user_id, a.get_user_id(s)) + self.assertEqual(self.project_id, a.get_project_id(s)) + + +class V3(CommonIdentityTests, utils.TestCase): + + @property + def version(self): + return 'v3' + + def get_auth_data(self, **kwargs): + token = fixture.V3Token(**kwargs) + region = 'RegionOne' + + svc = token.add_service('identity') + svc.add_standard_endpoints(admin=self.TEST_ADMIN_URL, region=region) + + svc = token.add_service('compute') + svc.add_standard_endpoints(admin=self.TEST_COMPUTE_ADMIN, + public=self.TEST_COMPUTE_PUBLIC, + internal=self.TEST_COMPUTE_INTERNAL, + region=region) + + return token + + def stub_auth(self, subject_token=None, **kwargs): + if not subject_token: + subject_token = self.TEST_TOKEN + + kwargs.setdefault('headers', {})['X-Subject-Token'] = subject_token + self.stub_url('POST', ['auth', 'tokens'], **kwargs) + + def create_auth_plugin(self, **kwargs): + kwargs.setdefault('auth_url', self.TEST_URL) + kwargs.setdefault('username', self.TEST_USER) + kwargs.setdefault('password', self.TEST_PASS) + return identity.V3Password(**kwargs) + + +class V2(CommonIdentityTests, utils.TestCase): + + @property + def version(self): + return 'v2.0' + + def create_auth_plugin(self, **kwargs): + kwargs.setdefault('auth_url', self.TEST_URL) + kwargs.setdefault('username', self.TEST_USER) + kwargs.setdefault('password', self.TEST_PASS) + return identity.V2Password(**kwargs) + + def get_auth_data(self, **kwargs): + token = fixture.V2Token(**kwargs) + region = 'RegionOne' + + svc = token.add_service('identity') + svc.add_endpoint(self.TEST_ADMIN_URL, region=region) + + svc = token.add_service('compute') + svc.add_endpoint(public=self.TEST_COMPUTE_PUBLIC, + internal=self.TEST_COMPUTE_INTERNAL, + admin=self.TEST_COMPUTE_ADMIN, + region=region) + + return token + + def stub_auth(self, **kwargs): + self.stub_url('POST', ['tokens'], **kwargs) + + +class CatalogHackTests(utils.TestCase): + + TEST_URL = 'http://keystone.server:5000/v2.0' + OTHER_URL = 'http://other.server:5000/path' + + IDENTITY = 'identity' + + BASE_URL = 'http://keystone.server:5000/' + V2_URL = BASE_URL + 'v2.0' + V3_URL = BASE_URL + 'v3' + + def setUp(self): + super(CatalogHackTests, self).setUp() + self.deprecations.expect_deprecations() + + def test_getting_endpoints(self): + disc = fixture.DiscoveryList(href=self.BASE_URL) + self.stub_url('GET', + ['/'], + base_url=self.BASE_URL, + json=disc) + + token = fixture.V2Token() + service = token.add_service(self.IDENTITY) + service.add_endpoint(public=self.V2_URL, + admin=self.V2_URL, + internal=self.V2_URL) + + self.stub_url('POST', + ['tokens'], + base_url=self.V2_URL, + json=token) + + v2_auth = identity.V2Password(self.V2_URL, + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + + sess = session.Session(auth=v2_auth) + + endpoint = sess.get_endpoint(service_type=self.IDENTITY, + interface='public', + version=(3, 0)) + + self.assertEqual(self.V3_URL, endpoint) + + def test_returns_original_when_discover_fails(self): + token = fixture.V2Token() + service = token.add_service(self.IDENTITY) + service.add_endpoint(public=self.V2_URL, + admin=self.V2_URL, + internal=self.V2_URL) + + self.stub_url('POST', + ['tokens'], + base_url=self.V2_URL, + json=token) + + self.stub_url('GET', [], base_url=self.BASE_URL, status_code=404) + + v2_auth = identity.V2Password(self.V2_URL, + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + + sess = session.Session(auth=v2_auth) + + endpoint = sess.get_endpoint(service_type=self.IDENTITY, + interface='public', + version=(3, 0)) + + self.assertEqual(self.V2_URL, endpoint) + + def test_getting_endpoints_on_auth_interface(self): + disc = fixture.DiscoveryList(href=self.BASE_URL) + self.stub_url('GET', + ['/'], + base_url=self.BASE_URL, + status_code=300, + json=disc) + + token = fixture.V2Token() + service = token.add_service(self.IDENTITY) + service.add_endpoint(public=self.V2_URL, + admin=self.V2_URL, + internal=self.V2_URL) + + self.stub_url('POST', + ['tokens'], + base_url=self.V2_URL, + json=token) + + v2_auth = identity.V2Password(self.V2_URL, + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + + sess = session.Session(auth=v2_auth) + + endpoint = sess.get_endpoint(interface=plugin.AUTH_INTERFACE, + version=(3, 0)) + + self.assertEqual(self.V3_URL, endpoint) + + +class GenericPlugin(base.BaseAuthPlugin): + + BAD_TOKEN = uuid.uuid4().hex + + def __init__(self): + super(GenericPlugin, self).__init__() + + self.endpoint = 'http://keystone.host:5000' + + self.headers = {'headerA': 'valueA', + 'headerB': 'valueB'} + + self.cert = '/path/to/cert' + self.connection_params = {'cert': self.cert, 'verify': False} + + def url(self, prefix): + return '%s/%s' % (self.endpoint, prefix) + + def get_token(self, session, **kwargs): + # NOTE(jamielennox): by specifying get_headers this should not be used + return self.BAD_TOKEN + + def get_headers(self, session, **kwargs): + return self.headers + + def get_endpoint(self, session, **kwargs): + return self.endpoint + + def get_connection_params(self, session, **kwargs): + return self.connection_params + + +class GenericAuthPluginTests(utils.TestCase): + + # filter doesn't matter to GenericPlugin, but we have to specify one + ENDPOINT_FILTER = {uuid.uuid4().hex: uuid.uuid4().hex} + + def setUp(self): + super(GenericAuthPluginTests, self).setUp() + self.auth = GenericPlugin() + + with self.deprecations.expect_deprecations_here(): + self.session = session.Session(auth=self.auth) + + def test_setting_headers(self): + text = uuid.uuid4().hex + self.stub_url('GET', base_url=self.auth.url('prefix'), text=text) + + resp = self.session.get('prefix', endpoint_filter=self.ENDPOINT_FILTER) + + self.assertEqual(text, resp.text) + + for k, v in self.auth.headers.items(): + self.assertRequestHeaderEqual(k, v) + + with self.deprecations.expect_deprecations_here(): + self.assertIsNone(self.session.get_token()) + self.assertEqual(self.auth.headers, + self.session.get_auth_headers()) + self.assertNotIn('X-Auth-Token', + self.requests_mock.last_request.headers) + + def test_setting_connection_params(self): + text = uuid.uuid4().hex + + with mock.patch.object(self.session.session, 'request') as mocked: + mocked.return_value = utils.test_response(text=text) + resp = self.session.get('prefix', + endpoint_filter=self.ENDPOINT_FILTER) + + self.assertEqual(text, resp.text) + + # the cert and verify values passed to request are those that were + # returned from the auth plugin as connection params. + + mocked.assert_called_once_with('GET', + self.auth.url('prefix'), + headers=mock.ANY, + allow_redirects=False, + cert=self.auth.cert, + verify=False) + + def test_setting_bad_connection_params(self): + # The uuid name parameter here is unknown and not in the allowed params + # to be returned to the session and so an error will be raised. + name = uuid.uuid4().hex + self.auth.connection_params[name] = uuid.uuid4().hex + + e = self.assertRaises(exceptions.UnsupportedParameters, + self.session.get, + 'prefix', + endpoint_filter=self.ENDPOINT_FILTER) + + self.assertIn(name, str(e)) diff --git a/keystoneclient/tests/unit/auth/test_identity_v2.py b/keystoneclient/tests/unit/auth/test_identity_v2.py new file mode 100644 index 000000000..84f4f5194 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_identity_v2.py @@ -0,0 +1,328 @@ +# 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 copy +from unittest import mock +import uuid + + +from keystoneclient.auth.identity import v2 +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class V2IdentityPlugin(utils.TestCase): + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0') + + TEST_PASS = 'password' + + TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "adminURL": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8774/v1.0", + "publicURL": "http://cdn.admin-nets.local:8774/v1.0/" + }], + "type": "nova_compat", + "name": "nova_compat" + }, { + "endpoints": [{ + "adminURL": "http://nova/novapi/admin", + "region": "RegionOne", + "internalURL": "http://nova/novapi/internal", + "publicURL": "http://nova/novapi/public" + }], + "type": "compute", + "name": "nova" + }, { + "endpoints": [{ + "adminURL": "http://glance/glanceapi/admin", + "region": "RegionOne", + "internalURL": "http://glance/glanceapi/internal", + "publicURL": "http://glance/glanceapi/public" + }], + "type": "image", + "name": "glance" + }, { + "endpoints": [{ + "adminURL": TEST_ADMIN_URL, + "region": "RegionOne", + "internalURL": "http://127.0.0.1:5000/v2.0", + "publicURL": "http://127.0.0.1:5000/v2.0" + }], + "type": "identity", + "name": "keystone" + }, { + "endpoints": [{ + "adminURL": "http://swift/swiftapi/admin", + "region": "RegionOne", + "internalURL": "http://swift/swiftapi/internal", + "publicURL": "http://swift/swiftapi/public" + }], + "type": "object-store", + "name": "swift" + }] + + def setUp(self): + super(V2IdentityPlugin, self).setUp() + self.deprecations.expect_deprecations() + self.TEST_RESPONSE_DICT = { + "access": { + "token": { + "expires": "2999-01-01T00:00:10.000123Z", + "id": self.TEST_TOKEN, + "tenant": { + "id": self.TEST_TENANT_ID + }, + }, + "user": { + "id": self.TEST_USER + }, + "serviceCatalog": self.TEST_SERVICE_CATALOG, + }, + } + + def stub_auth(self, **kwargs): + self.stub_url('POST', ['tokens'], **kwargs) + + def test_authenticate_with_username_password(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + self.assertIsNone(a.user_id) + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, + 'password': self.TEST_PASS}}} + self.assertRequestBodyIs(json=req) + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('Accept', 'application/json') + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_user_id_password(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Password(self.TEST_URL, user_id=self.TEST_USER, + password=self.TEST_PASS) + self.assertIsNone(a.username) + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER, + 'password': self.TEST_PASS}}} + self.assertRequestBodyIs(json=req) + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('Accept', 'application/json') + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_username_password_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID) + self.assertIsNone(a.user_id) + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, + 'password': self.TEST_PASS}, + 'tenantId': self.TEST_TENANT_ID}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_user_id_password_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Password(self.TEST_URL, user_id=self.TEST_USER, + password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID) + self.assertIsNone(a.username) + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER, + 'password': self.TEST_PASS}, + 'tenantId': self.TEST_TENANT_ID}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_token(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Token(self.TEST_URL, 'foo') + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'token': {'id': 'foo'}}} + self.assertRequestBodyIs(json=req) + self.assertRequestHeaderEqual('x-Auth-Token', 'foo') + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('Accept', 'application/json') + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_with_trust_id(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS, trust_id='trust') + s = session.Session(a) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, + 'password': self.TEST_PASS}, + 'trust_id': 'trust'}} + + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def _do_service_url_test(self, base_url, endpoint_filter): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', ['path'], + base_url=base_url, + text='SUCCESS', status_code=200) + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + resp = s.get('/path', endpoint_filter=endpoint_filter) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(self.requests_mock.last_request.url, + base_url + '/path') + + def test_service_url(self): + endpoint_filter = {'service_type': 'compute', + 'interface': 'admin', + 'service_name': 'nova'} + self._do_service_url_test('http://nova/novapi/admin', endpoint_filter) + + def test_service_url_defaults_to_public(self): + endpoint_filter = {'service_type': 'compute'} + self._do_service_url_test('http://nova/novapi/public', endpoint_filter) + + def test_endpoint_filter_without_service_type_fails(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.EndpointNotFound, s.get, '/path', + endpoint_filter={'interface': 'admin'}) + + def test_full_url_overrides_endpoint_filter(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [], + base_url='http://testurl/', + text='SUCCESS', status_code=200) + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + resp = s.get('http://testurl/', + endpoint_filter={'service_type': 'compute'}) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.text, 'SUCCESS') + + def test_invalid_auth_response_dict(self): + self.stub_auth(json={'hello': 'world'}) + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', + authenticated=True) + + def test_invalid_auth_response_type(self): + self.stub_url('POST', ['tokens'], text='testdata') + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', + authenticated=True) + + def test_invalidate_response(self): + resp_data1 = copy.deepcopy(self.TEST_RESPONSE_DICT) + resp_data2 = copy.deepcopy(self.TEST_RESPONSE_DICT) + + resp_data1['access']['token']['id'] = 'token1' + resp_data2['access']['token']['id'] = 'token2' + + auth_responses = [{'json': resp_data1}, {'json': resp_data2}] + self.stub_auth(response_list=auth_responses) + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + with self.deprecations.expect_deprecations_here(): + self.assertEqual('token1', s.get_token()) + self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers()) + + a.invalidate() + with self.deprecations.expect_deprecations_here(): + self.assertEqual('token2', s.get_token()) + self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers()) + + def test_doesnt_log_password(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + password = uuid.uuid4().hex + + a = v2.Password(self.TEST_URL, username=self.TEST_USER, + password=password) + s = session.Session(auth=a) + with self.deprecations.expect_deprecations_here(): + self.assertEqual(self.TEST_TOKEN, s.get_token()) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + self.assertNotIn(password, self.logger.output) + + def test_password_with_no_user_id_or_name(self): + self.assertRaises(TypeError, + v2.Password, self.TEST_URL, password=self.TEST_PASS) + + @mock.patch('sys.stdin', autospec=True) + def test_prompt_password(self, mock_stdin): + parser = argparse.ArgumentParser() + v2.Password.register_argparse_arguments(parser) + + username = uuid.uuid4().hex + auth_url = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + password = uuid.uuid4().hex + + opts = parser.parse_args(['--os-username', username, + '--os-auth-url', auth_url, + '--os-tenant-id', tenant_id]) + + with mock.patch('getpass.getpass') as mock_getpass: + mock_getpass.return_value = password + mock_stdin.isatty = lambda: True + + plugin = v2.Password.load_from_argparse_arguments(opts) + + self.assertEqual(auth_url, plugin.auth_url) + self.assertEqual(username, plugin.username) + self.assertEqual(tenant_id, plugin.tenant_id) + self.assertEqual(password, plugin.password) diff --git a/keystoneclient/tests/unit/auth/test_identity_v3.py b/keystoneclient/tests/unit/auth/test_identity_v3.py new file mode 100644 index 000000000..fe7815ed2 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_identity_v3.py @@ -0,0 +1,570 @@ +# 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 copy +from unittest import mock +import uuid + +from keystoneauth1 import fixture + +from keystoneclient import access +from keystoneclient.auth.identity import v3 +from keystoneclient.auth.identity.v3 import base as v3_base +from keystoneclient import client +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class V3IdentityPlugin(utils.TestCase): + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') + + TEST_PASS = 'password' + + TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "url": "http://cdn.admin-nets.local:8774/v1.0/", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://127.0.0.1:8774/v1.0", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "interface": "admin" + }], + "type": "nova_compat" + }, { + "endpoints": [{ + "url": "http://nova/novapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://nova/novapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://nova/novapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "compute", + "name": "nova", + }, { + "endpoints": [{ + "url": "http://glance/glanceapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://glance/glanceapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://glance/glanceapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "image", + "name": "glance" + }, { + "endpoints": [{ + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne", + "interface": "internal" + }, { + "url": TEST_ADMIN_URL, + "region": "RegionOne", + "interface": "admin" + }], + "type": "identity" + }, { + "endpoints": [{ + "url": "http://swift/swiftapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://swift/swiftapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://swift/swiftapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "object-store" + }] + + def setUp(self): + super(V3IdentityPlugin, self).setUp() + + self.TEST_DISCOVERY_RESPONSE = { + 'versions': {'values': [fixture.V3Discovery(self.TEST_URL)]}} + + self.deprecations.expect_deprecations() + + self.TEST_RESPONSE_DICT = { + "token": { + "methods": [ + "token", + "password" + ], + + "expires_at": "2999-01-01T00:00:10.000123Z", + "project": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_TENANT_ID, + "name": self.TEST_TENANT_NAME + }, + "user": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_USER, + "name": self.TEST_USER + }, + "issued_at": "2013-05-29T16:55:21.468960Z", + "catalog": self.TEST_SERVICE_CATALOG + }, + } + self.TEST_PROJECTS_RESPONSE = { + "projects": [ + { + "domain_id": "1789d1", + "enabled": "True", + "id": "263fd9", + "links": { + "self": "https://identity:5000/v3/projects/263fd9" + }, + "name": "Dev Group A" + }, + { + "domain_id": "1789d1", + "enabled": "True", + "id": "e56ad3", + "links": { + "self": "https://identity:5000/v3/projects/e56ad3" + }, + "name": "Dev Group B" + } + ], + "links": { + "self": "https://identity:5000/v3/projects", + } + } + + def stub_auth(self, subject_token=None, **kwargs): + if not subject_token: + subject_token = self.TEST_TOKEN + + self.stub_url('POST', ['auth', 'tokens'], + headers={'X-Subject-Token': subject_token}, **kwargs) + + def test_authenticate_with_username_password(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v3.Password(self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}}}} + + self.assertRequestBodyIs(json=req) + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('Accept', 'application/json') + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_username_password_unscoped(self): + del self.TEST_RESPONSE_DICT['token']['catalog'] + del self.TEST_RESPONSE_DICT['token']['project'] + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url(method="GET", json=self.TEST_DISCOVERY_RESPONSE) + test_user_id = self.TEST_RESPONSE_DICT['token']['user']['id'] + self.stub_url(method="GET", + json=self.TEST_PROJECTS_RESPONSE, + parts=['users', test_user_id, 'projects']) + + a = v3.Password(self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + cs = client.Client(session=s) + + # As a sanity check on the auth_ref, make sure client has the + # proper user id, that it fetches the right project response + self.assertEqual(test_user_id, a.auth_ref.user_id) + t = cs.projects.list(user=a.auth_ref.user_id) + self.assertEqual(2, len(t)) + + def test_authenticate_with_username_password_domain_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS, domain_id=self.TEST_DOMAIN_ID) + s = session.Session(a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}}, + 'scope': {'domain': {'id': self.TEST_DOMAIN_ID}}}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_authenticate_with_username_password_project_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS, + project_id=self.TEST_TENANT_ID) + s = session.Session(a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}}, + 'scope': {'project': {'id': self.TEST_TENANT_ID}}}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + self.assertEqual(s.auth.auth_ref.project_id, self.TEST_TENANT_ID) + + def test_authenticate_with_token(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v3.Token(self.TEST_URL, self.TEST_TOKEN) + s = session.Session(auth=a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['token'], + 'token': {'id': self.TEST_TOKEN}}}} + + self.assertRequestBodyIs(json=req) + + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('Accept', 'application/json') + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_with_expired(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + d = copy.deepcopy(self.TEST_RESPONSE_DICT) + d['token']['expires_at'] = '2000-01-01T00:00:10.000123Z' + + a = v3.Password(self.TEST_URL, username='username', + password='password') + a.auth_ref = access.AccessInfo.factory(body=d) + s = session.Session(auth=a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + self.assertEqual(a.auth_ref['expires_at'], + self.TEST_RESPONSE_DICT['token']['expires_at']) + + def test_with_domain_and_project_scoping(self): + a = v3.Password(self.TEST_URL, username='username', + password='password', project_id='project', + domain_id='domain') + + self.assertRaises(exceptions.AuthorizationFailure, + a.get_token, None) + self.assertRaises(exceptions.AuthorizationFailure, + a.get_headers, None) + + def test_with_trust_id(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS, trust_id='trust') + s = session.Session(a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}}, + 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_with_multiple_mechanisms_factory(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + p = v3.PasswordMethod(username=self.TEST_USER, password=self.TEST_PASS) + t = v3.TokenMethod(token='foo') + a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust') + s = session.Session(a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password', 'token'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}, + 'token': {'id': 'foo'}}, + 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_with_multiple_mechanisms(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + p = v3.PasswordMethod(username=self.TEST_USER, + password=self.TEST_PASS) + t = v3.TokenMethod(token='foo') + a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust') + s = session.Session(auth=a) + + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + req = {'auth': {'identity': + {'methods': ['password', 'token'], + 'password': {'user': {'name': self.TEST_USER, + 'password': self.TEST_PASS}}, + 'token': {'id': 'foo'}}, + 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + + def test_with_multiple_scopes(self): + s = session.Session() + + a = v3.Password(self.TEST_URL, + username=self.TEST_USER, password=self.TEST_PASS, + domain_id='x', project_id='x') + self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) + + a = v3.Password(self.TEST_URL, + username=self.TEST_USER, password=self.TEST_PASS, + domain_id='x', trust_id='x') + self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) + + def _do_service_url_test(self, base_url, endpoint_filter): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', ['path'], + base_url=base_url, + text='SUCCESS', status_code=200) + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + resp = s.get('/path', endpoint_filter=endpoint_filter) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(self.requests_mock.last_request.url, + base_url + '/path') + + def test_service_url(self): + endpoint_filter = {'service_type': 'compute', + 'interface': 'admin', + 'service_name': 'nova'} + self._do_service_url_test('http://nova/novapi/admin', endpoint_filter) + + def test_service_url_defaults_to_public(self): + endpoint_filter = {'service_type': 'compute'} + self._do_service_url_test('http://nova/novapi/public', endpoint_filter) + + def test_endpoint_filter_without_service_type_fails(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.EndpointNotFound, s.get, '/path', + endpoint_filter={'interface': 'admin'}) + + def test_full_url_overrides_endpoint_filter(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [], + base_url='http://testurl/', + text='SUCCESS', status_code=200) + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + resp = s.get('http://testurl/', + endpoint_filter={'service_type': 'compute'}) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.text, 'SUCCESS') + + def test_invalid_auth_response_dict(self): + self.stub_auth(json={'hello': 'world'}) + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', + authenticated=True) + + def test_invalid_auth_response_type(self): + self.stub_url('POST', ['auth', 'tokens'], text='testdata') + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', + authenticated=True) + + def test_invalidate_response(self): + auth_responses = [{'status_code': 200, 'json': self.TEST_RESPONSE_DICT, + 'headers': {'X-Subject-Token': 'token1'}}, + {'status_code': 200, 'json': self.TEST_RESPONSE_DICT, + 'headers': {'X-Subject-Token': 'token2'}}] + + self.requests_mock.post('%s/auth/tokens' % self.TEST_URL, + auth_responses) + + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=self.TEST_PASS) + s = session.Session(auth=a) + + with self.deprecations.expect_deprecations_here(): + self.assertEqual('token1', s.get_token()) + self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers()) + a.invalidate() + with self.deprecations.expect_deprecations_here(): + self.assertEqual('token2', s.get_token()) + self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers()) + + def test_doesnt_log_password(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + password = uuid.uuid4().hex + a = v3.Password(self.TEST_URL, username=self.TEST_USER, + password=password) + s = session.Session(a) + with self.deprecations.expect_deprecations_here(): + self.assertEqual(self.TEST_TOKEN, s.get_token()) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + + self.assertNotIn(password, self.logger.output) + + def test_sends_nocatalog(self): + del self.TEST_RESPONSE_DICT['token']['catalog'] + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + a = v3.Password(self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_PASS, + include_catalog=False) + s = session.Session(auth=a) + + s.get_auth_headers() + + auth_url = self.TEST_URL + '/auth/tokens' + self.assertEqual(auth_url, a.token_url) + self.assertEqual(auth_url + '?nocatalog', + self.requests_mock.last_request.url) + + def test_symbols(self): + self.assertIs(v3.AuthMethod, v3_base.AuthMethod) + self.assertIs(v3.AuthConstructor, v3_base.AuthConstructor) + self.assertIs(v3.Auth, v3_base.Auth) + + def test_unscoped_request(self): + token = fixture.V3Token() + self.stub_auth(json=token) + password = uuid.uuid4().hex + + a = v3.Password(self.TEST_URL, + user_id=token.user_id, + password=password, + unscoped=True) + s = session.Session() + + auth_ref = a.get_access(s) + + with self.deprecations.expect_deprecations_here(): + self.assertFalse(auth_ref.scoped) + body = self.requests_mock.last_request.json() + + ident = body['auth']['identity'] + + self.assertEqual(['password'], ident['methods']) + self.assertEqual(token.user_id, ident['password']['user']['id']) + self.assertEqual(password, ident['password']['user']['password']) + + self.assertEqual({}, body['auth']['scope']['unscoped']) + + def test_unscoped_with_scope_data(self): + a = v3.Password(self.TEST_URL, + user_id=uuid.uuid4().hex, + password=uuid.uuid4().hex, + unscoped=True, + project_id=uuid.uuid4().hex) + + s = session.Session() + + self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) + + @mock.patch('sys.stdin', autospec=True) + def test_prompt_password(self, mock_stdin): + parser = argparse.ArgumentParser() + v3.Password.register_argparse_arguments(parser) + + username = uuid.uuid4().hex + user_domain_id = uuid.uuid4().hex + auth_url = uuid.uuid4().hex + project_id = uuid.uuid4().hex + password = uuid.uuid4().hex + + opts = parser.parse_args(['--os-username', username, + '--os-auth-url', auth_url, + '--os-user-domain-id', user_domain_id, + '--os-project-id', project_id]) + + with mock.patch('getpass.getpass') as mock_getpass: + mock_getpass.return_value = password + mock_stdin.isatty = lambda: True + + plugin = v3.Password.load_from_argparse_arguments(opts) + + self.assertEqual(auth_url, plugin.auth_url) + self.assertEqual(username, plugin.auth_methods[0].username) + self.assertEqual(project_id, plugin.project_id) + self.assertEqual(user_domain_id, + plugin.auth_methods[0].user_domain_id) + self.assertEqual(password, plugin.auth_methods[0].password) diff --git a/keystoneclient/tests/unit/auth/test_identity_v3_federated.py b/keystoneclient/tests/unit/auth/test_identity_v3_federated.py new file mode 100644 index 000000000..7cbb5ab1e --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_identity_v3_federated.py @@ -0,0 +1,99 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import uuid + +from keystoneauth1 import fixture + +from keystoneclient import access +from keystoneclient.auth.identity import v3 +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class TesterFederationPlugin(v3.FederatedBaseAuth): + + def get_unscoped_auth_ref(self, sess, **kwargs): + # This would go and talk to an idp or something + resp = sess.post(self.federated_token_url, authenticated=False) + return access.AccessInfo.factory(resp=resp, body=resp.json()) + + +class V3FederatedPlugin(utils.TestCase): + + AUTH_URL = 'http://keystone/v3' + + def setUp(self): + super(V3FederatedPlugin, self).setUp() + + self.deprecations.expect_deprecations() + + self.unscoped_token = fixture.V3Token() + self.unscoped_token_id = uuid.uuid4().hex + self.scoped_token = copy.deepcopy(self.unscoped_token) + self.scoped_token.set_project_scope() + self.scoped_token.methods.append('token') + self.scoped_token_id = uuid.uuid4().hex + + s = self.scoped_token.add_service('compute', name='nova') + s.add_standard_endpoints(public='http://nova/public', + admin='http://nova/admin', + internal='http://nova/internal') + + self.idp = uuid.uuid4().hex + self.protocol = uuid.uuid4().hex + + self.token_url = ('%s/OS-FEDERATION/identity_providers/%s/protocols/%s' + '/auth' % (self.AUTH_URL, self.idp, self.protocol)) + + headers = {'X-Subject-Token': self.unscoped_token_id} + self.unscoped_mock = self.requests_mock.post(self.token_url, + json=self.unscoped_token, + headers=headers) + + headers = {'X-Subject-Token': self.scoped_token_id} + auth_url = self.AUTH_URL + '/auth/tokens' + self.scoped_mock = self.requests_mock.post(auth_url, + json=self.scoped_token, + headers=headers) + + def get_plugin(self, **kwargs): + kwargs.setdefault('auth_url', self.AUTH_URL) + kwargs.setdefault('protocol', self.protocol) + kwargs.setdefault('identity_provider', self.idp) + return TesterFederationPlugin(**kwargs) + + def test_federated_url(self): + plugin = self.get_plugin() + self.assertEqual(self.token_url, plugin.federated_token_url) + + def test_unscoped_behaviour(self): + sess = session.Session(auth=self.get_plugin()) + self.assertEqual(self.unscoped_token_id, sess.get_token()) + + self.assertTrue(self.unscoped_mock.called) + self.assertFalse(self.scoped_mock.called) + + def test_scoped_behaviour(self): + auth = self.get_plugin(project_id=self.scoped_token.project_id) + sess = session.Session(auth=auth) + self.assertEqual(self.scoped_token_id, sess.get_token()) + + self.assertTrue(self.unscoped_mock.called) + self.assertTrue(self.scoped_mock.called) + + def test_options(self): + opts = [o.name for o in v3.FederatedBaseAuth.get_options()] + + self.assertIn('protocol', opts) + self.assertIn('identity-provider', opts) diff --git a/keystoneclient/tests/unit/auth/test_loading.py b/keystoneclient/tests/unit/auth/test_loading.py new file mode 100644 index 000000000..3c2689dd9 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_loading.py @@ -0,0 +1,46 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + + +from keystoneclient.tests.unit.auth import utils + + +class TestOtherLoading(utils.TestCase): + + def test_loading_getter(self): + + called_opts = [] + + vals = {'a-int': 44, + 'a-bool': False, + 'a-float': 99.99, + 'a-str': 'value'} + + val = uuid.uuid4().hex + + def _getter(opt): + called_opts.append(opt.name) + # return str because oslo.config should convert them back + return str(vals[opt.name]) + + p = utils.MockPlugin.load_from_options_getter(_getter, other=val) + + self.assertEqual(set(vals), set(called_opts)) + + for k, v in vals.items(): + # replace - to _ because it's the dest used to create kwargs + self.assertEqual(v, p[k.replace('-', '_')]) + + # check that additional kwargs get passed through + self.assertEqual(val, p['other']) diff --git a/keystoneclient/tests/unit/auth/test_password.py b/keystoneclient/tests/unit/auth/test_password.py new file mode 100644 index 000000000..93d2fb8ca --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_password.py @@ -0,0 +1,99 @@ +# 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 +from unittest import mock +import uuid + + +from keystoneclient.auth.identity.generic import password +from keystoneclient.auth.identity import v2 +from keystoneclient.auth.identity import v3 +from keystoneclient.auth.identity.v3 import password as v3_password +from keystoneclient.tests.unit.auth import utils + + +class PasswordTests(utils.GenericPluginTestCase): + + PLUGIN_CLASS = password.Password + V2_PLUGIN_CLASS = v2.Password + V3_PLUGIN_CLASS = v3.Password + + def new_plugin(self, **kwargs): + kwargs.setdefault('username', uuid.uuid4().hex) + kwargs.setdefault('password', uuid.uuid4().hex) + return super(PasswordTests, self).new_plugin(**kwargs) + + def test_with_user_domain_params(self): + self.stub_discovery() + + self.assertCreateV3(domain_id=uuid.uuid4().hex, + user_domain_id=uuid.uuid4().hex) + + def test_v3_user_params_v2_url(self): + self.stub_discovery(v3=False) + self.assertDiscoveryFailure(user_domain_id=uuid.uuid4().hex) + + def test_options(self): + opts = [o.name for o in self.PLUGIN_CLASS.get_options()] + + allowed_opts = ['username', + 'user-domain-id', + 'user-domain-name', + 'user-id', + 'password', + + 'domain-id', + 'domain-name', + 'tenant-id', + 'tenant-name', + 'project-id', + 'project-name', + 'project-domain-id', + 'project-domain-name', + 'trust-id', + 'auth-url'] + + self.assertEqual(set(allowed_opts), set(opts)) + self.assertEqual(len(allowed_opts), len(opts)) + + def test_symbols(self): + self.assertIs(v3.Password, v3_password.Password) + self.assertIs(v3.PasswordMethod, v3_password.PasswordMethod) + + @mock.patch('sys.stdin', autospec=True) + def test_prompt_password(self, mock_stdin): + parser = argparse.ArgumentParser() + self.PLUGIN_CLASS.register_argparse_arguments(parser) + + username = uuid.uuid4().hex + user_domain_id = uuid.uuid4().hex + auth_url = uuid.uuid4().hex + project_id = uuid.uuid4().hex + password = uuid.uuid4().hex + + opts = parser.parse_args(['--os-username', username, + '--os-auth-url', auth_url, + '--os-user-domain-id', user_domain_id, + '--os-project-id', project_id]) + + with mock.patch('getpass.getpass') as mock_getpass: + mock_getpass.return_value = password + mock_stdin.isatty = lambda: True + + plugin = self.PLUGIN_CLASS.load_from_argparse_arguments(opts) + + self.assertEqual(auth_url, plugin.auth_url) + self.assertEqual(username, plugin._username) + self.assertEqual(project_id, plugin._project_id) + self.assertEqual(user_domain_id, plugin._user_domain_id) + self.assertEqual(password, plugin._password) diff --git a/keystoneclient/tests/unit/auth/test_token.py b/keystoneclient/tests/unit/auth/test_token.py new file mode 100644 index 000000000..ce4c1cdc6 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_token.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.auth.identity.generic import token +from keystoneclient.auth.identity import v2 +from keystoneclient.auth.identity import v3 +from keystoneclient.auth.identity.v3 import token as v3_token +from keystoneclient.tests.unit.auth import utils + + +class TokenTests(utils.GenericPluginTestCase): + + PLUGIN_CLASS = token.Token + V2_PLUGIN_CLASS = v2.Token + V3_PLUGIN_CLASS = v3.Token + + def new_plugin(self, **kwargs): + kwargs.setdefault('token', uuid.uuid4().hex) + return super(TokenTests, self).new_plugin(**kwargs) + + def test_options(self): + opts = [o.name for o in self.PLUGIN_CLASS.get_options()] + + allowed_opts = ['token', + 'domain-id', + 'domain-name', + 'tenant-id', + 'tenant-name', + 'project-id', + 'project-name', + 'project-domain-id', + 'project-domain-name', + 'trust-id', + 'auth-url'] + + self.assertEqual(set(allowed_opts), set(opts)) + self.assertEqual(len(allowed_opts), len(opts)) + + def test_symbols(self): + self.assertIs(v3.Token, v3_token.Token) + self.assertIs(v3.TokenMethod, v3_token.TokenMethod) diff --git a/keystoneclient/tests/unit/auth/test_token_endpoint.py b/keystoneclient/tests/unit/auth/test_token_endpoint.py new file mode 100644 index 000000000..9a9a1ed90 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_token_endpoint.py @@ -0,0 +1,67 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from testtools import matchers + +from keystoneclient.auth import token_endpoint +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class TokenEndpointTest(utils.TestCase): + + TEST_TOKEN = 'aToken' + TEST_URL = 'http://server/prefix' + + def setUp(self): + super(TokenEndpointTest, self).setUp() + self.deprecations.expect_deprecations() + + def test_basic_case(self): + self.requests_mock.get(self.TEST_URL, text='body') + + a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) + s = session.Session(auth=a) + + data = s.get(self.TEST_URL, authenticated=True) + + self.assertEqual(data.text, 'body') + self.assertRequestHeaderEqual('X-Auth-Token', self.TEST_TOKEN) + + def test_basic_endpoint_case(self): + self.stub_url('GET', ['p'], text='body') + a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) + s = session.Session(auth=a) + + data = s.get('/p', + authenticated=True, + endpoint_filter={'service': 'identity'}) + + self.assertEqual(self.TEST_URL, a.get_endpoint(s)) + self.assertEqual('body', data.text) + self.assertRequestHeaderEqual('X-Auth-Token', self.TEST_TOKEN) + + def test_token_endpoint_options(self): + opt_names = [opt.name for opt in token_endpoint.Token.get_options()] + + self.assertThat(opt_names, matchers.HasLength(2)) + + self.assertIn('token', opt_names) + self.assertIn('endpoint', opt_names) + + def test_token_endpoint_user_id(self): + a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) + s = session.Session() + + # we can't know this information about this sort of plugin + self.assertIsNone(a.get_user_id(s)) + self.assertIsNone(a.get_project_id(s)) diff --git a/keystoneclient/tests/unit/auth/utils.py b/keystoneclient/tests/unit/auth/utils.py new file mode 100644 index 000000000..1e346e35c --- /dev/null +++ b/keystoneclient/tests/unit/auth/utils.py @@ -0,0 +1,202 @@ +# 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 unittest import mock +import uuid + +from keystoneauth1 import fixture +from oslo_config import cfg + +from keystoneclient import access +from keystoneclient.auth import base +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +class MockPlugin(base.BaseAuthPlugin): + + INT_DESC = 'test int' + FLOAT_DESC = 'test float' + BOOL_DESC = 'test bool' + STR_DESC = 'test str' + STR_DEFAULT = uuid.uuid4().hex + + def __init__(self, **kwargs): + self._data = kwargs + + def __getitem__(self, key): + """Get the data of the key.""" + return self._data[key] + + def get_token(self, *args, **kwargs): + return 'aToken' + + def get_endpoint(self, *args, **kwargs): + return 'http://test' + + @classmethod + def get_options(cls): + return [ + cfg.IntOpt('a-int', default='3', help=cls.INT_DESC), + cfg.BoolOpt('a-bool', help=cls.BOOL_DESC), + cfg.FloatOpt('a-float', help=cls.FLOAT_DESC), + cfg.StrOpt('a-str', help=cls.STR_DESC, default=cls.STR_DEFAULT), + ] + + +class MockManager(object): + + def __init__(self, driver): + self.driver = driver + + +def mock_plugin(f): + @functools.wraps(f) + def inner(*args, **kwargs): + with mock.patch.object(base, 'get_plugin_class') as m: + m.return_value = MockPlugin + args = list(args) + [m] + return f(*args, **kwargs) + + return inner + + +class TestCase(utils.TestCase): + + GROUP = 'auth' + V2PASS = 'v2password' + V3TOKEN = 'v3token' + + a_int = 88 + a_float = 88.8 + a_bool = False + + TEST_VALS = {'a_int': a_int, + 'a_float': a_float, + 'a_bool': a_bool} + + def assertTestVals(self, plugin, vals=TEST_VALS): + for k, v in vals.items(): + self.assertEqual(v, plugin[k]) + + +class GenericPluginTestCase(utils.TestCase): + + TEST_URL = 'http://keystone.host:5000/' + + # OVERRIDE THESE IN SUB CLASSES + PLUGIN_CLASS = None + V2_PLUGIN_CLASS = None + V3_PLUGIN_CLASS = None + + def setUp(self): + super(GenericPluginTestCase, self).setUp() + + self.token_v2 = fixture.V2Token() + self.token_v3 = fixture.V3Token() + self.token_v3_id = uuid.uuid4().hex + + self.deprecations.expect_deprecations() + self.session = session.Session() + + self.stub_url('POST', ['v2.0', 'tokens'], json=self.token_v2) + self.stub_url('POST', ['v3', 'auth', 'tokens'], + headers={'X-Subject-Token': self.token_v3_id}, + json=self.token_v3) + + def new_plugin(self, **kwargs): + kwargs.setdefault('auth_url', self.TEST_URL) + return self.PLUGIN_CLASS(**kwargs) + + def stub_discovery(self, base_url=None, **kwargs): + kwargs.setdefault('href', self.TEST_URL) + disc = fixture.DiscoveryList(**kwargs) + self.stub_url('GET', json=disc, base_url=base_url, status_code=300) + return disc + + def assertCreateV3(self, **kwargs): + auth = self.new_plugin(**kwargs) + auth_ref = auth.get_auth_ref(self.session) + self.assertIsInstance(auth_ref, access.AccessInfoV3) + self.assertEqual(self.TEST_URL + 'v3/auth/tokens', + self.requests_mock.last_request.url) + self.assertIsInstance(auth._plugin, self.V3_PLUGIN_CLASS) + return auth + + def assertCreateV2(self, **kwargs): + auth = self.new_plugin(**kwargs) + auth_ref = auth.get_auth_ref(self.session) + self.assertIsInstance(auth_ref, access.AccessInfoV2) + self.assertEqual(self.TEST_URL + 'v2.0/tokens', + self.requests_mock.last_request.url) + self.assertIsInstance(auth._plugin, self.V2_PLUGIN_CLASS) + return auth + + def assertDiscoveryFailure(self, **kwargs): + plugin = self.new_plugin(**kwargs) + self.assertRaises(exceptions.DiscoveryFailure, + plugin.get_auth_ref, + self.session) + + def test_create_v3_if_domain_params(self): + self.stub_discovery() + + self.assertCreateV3(domain_id=uuid.uuid4().hex) + self.assertCreateV3(domain_name=uuid.uuid4().hex) + self.assertCreateV3(project_name=uuid.uuid4().hex, + project_domain_name=uuid.uuid4().hex) + self.assertCreateV3(project_name=uuid.uuid4().hex, + project_domain_id=uuid.uuid4().hex) + + def test_create_v2_if_no_domain_params(self): + self.stub_discovery() + self.assertCreateV2() + self.assertCreateV2(project_id=uuid.uuid4().hex) + self.assertCreateV2(project_name=uuid.uuid4().hex) + self.assertCreateV2(tenant_id=uuid.uuid4().hex) + self.assertCreateV2(tenant_name=uuid.uuid4().hex) + + def test_v3_params_v2_url(self): + self.stub_discovery(v3=False) + self.assertDiscoveryFailure(domain_name=uuid.uuid4().hex) + + def test_v2_params_v3_url(self): + self.stub_discovery(v2=False) + self.assertCreateV3() + + def test_no_urls(self): + self.stub_discovery(v2=False, v3=False) + self.assertDiscoveryFailure() + + def test_path_based_url_v2(self): + self.stub_url('GET', ['v2.0'], status_code=403) + self.assertCreateV2(auth_url=self.TEST_URL + 'v2.0') + + def test_path_based_url_v3(self): + self.stub_url('GET', ['v3'], status_code=403) + self.assertCreateV3(auth_url=self.TEST_URL + 'v3') + + def test_disc_error_for_failure(self): + self.stub_url('GET', [], status_code=403) + self.assertDiscoveryFailure() + + def test_v3_plugin_from_failure(self): + url = self.TEST_URL + 'v3' + self.stub_url('GET', [], base_url=url, status_code=403) + self.assertCreateV3(auth_url=url) + + def test_unknown_discovery_version(self): + # make a v4 entry that's mostly the same as a v3 + self.stub_discovery(v2=False, v3_id='v4.0') + self.assertDiscoveryFailure() diff --git a/keystoneclient/tests/unit/client_fixtures.py b/keystoneclient/tests/unit/client_fixtures.py new file mode 100644 index 000000000..c6c291984 --- /dev/null +++ b/keystoneclient/tests/unit/client_fixtures.py @@ -0,0 +1,743 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import contextlib +import os +import uuid +import warnings + +import fixtures +from keystoneauth1 import fixture +from keystoneauth1 import identity as ksa_identity +from keystoneauth1 import session as ksa_session +from oslo_serialization import jsonutils +from oslo_utils import timeutils +import testresources + +from keystoneclient.auth import identity as ksc_identity +from keystoneclient.common import cms +from keystoneclient import session as ksc_session +from keystoneclient import utils +from keystoneclient.v2_0 import client as v2_client +from keystoneclient.v3 import client as v3_client + + +TEST_ROOT_URL = 'http://127.0.0.1:5000/' + +TESTDIR = os.path.dirname(os.path.abspath(__file__)) +ROOTDIR = os.path.normpath(os.path.join(TESTDIR, '..', '..', '..')) +CERTDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'certs') +CMSDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'cms') +KEYDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'private') + + +class BaseFixture(fixtures.Fixture): + + TEST_ROOT_URL = TEST_ROOT_URL + + def __init__(self, requests, deprecations): + super(BaseFixture, self).__init__() + self.requests = requests + self.deprecations = deprecations + self.user_id = uuid.uuid4().hex + self.client = self.new_client() + + +class BaseV2(BaseFixture): + + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0') + + +class OriginalV2(BaseV2): + + def new_client(self): + # Creating a Client not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + return v2_client.Client(username=uuid.uuid4().hex, + user_id=self.user_id, + token=uuid.uuid4().hex, + tenant_name=uuid.uuid4().hex, + auth_url=self.TEST_URL, + endpoint=self.TEST_URL) + + +class KscSessionV2(BaseV2): + + def new_client(self): + t = fixture.V2Token(user_id=self.user_id) + t.set_scope() + + s = t.add_service('identity') + s.add_endpoint(self.TEST_URL) + + d = fixture.V2Discovery(self.TEST_URL) + + self.requests.register_uri('POST', self.TEST_URL + '/tokens', json=t) + + # NOTE(jamielennox): Because of the versioned URL hack here even though + # the V2 URL will be in the service catalog it will be the root URL + # that will be queried for discovery. + self.requests.register_uri('GET', self.TEST_ROOT_URL, + json={'version': d}) + + with self.deprecations.expect_deprecations_here(): + a = ksc_identity.V2Password(username=uuid.uuid4().hex, + password=uuid.uuid4().hex, + auth_url=self.TEST_URL) + + s = ksc_session.Session(auth=a) + + return v2_client.Client(session=s) + + +class KsaSessionV2(BaseV2): + + def new_client(self): + t = fixture.V2Token(user_id=self.user_id) + t.set_scope() + + s = t.add_service('identity') + s.add_endpoint(self.TEST_URL) + + d = fixture.V2Discovery(self.TEST_URL) + + self.requests.register_uri('POST', self.TEST_URL + '/tokens', json=t) + + # NOTE(jamielennox): Because of the versioned URL hack here even though + # the V2 URL will be in the service catalog it will be the root URL + # that will be queried for discovery. + self.requests.register_uri('GET', self.TEST_ROOT_URL, + json={'version': d}) + + a = ksa_identity.V2Password(username=uuid.uuid4().hex, + password=uuid.uuid4().hex, + auth_url=self.TEST_URL) + s = ksa_session.Session(auth=a) + return v2_client.Client(session=s) + + +class BaseV3(BaseFixture): + + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + + +class OriginalV3(BaseV3): + + def new_client(self): + # Creating a Client not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + return v3_client.Client(username=uuid.uuid4().hex, + user_id=self.user_id, + token=uuid.uuid4().hex, + tenant_name=uuid.uuid4().hex, + auth_url=self.TEST_URL, + endpoint=self.TEST_URL) + + +class KscSessionV3(BaseV3): + + def new_client(self): + t = fixture.V3Token(user_id=self.user_id) + t.set_project_scope() + + s = t.add_service('identity') + s.add_standard_endpoints(public=self.TEST_URL, + admin=self.TEST_URL) + + d = fixture.V3Discovery(self.TEST_URL) + + headers = {'X-Subject-Token': uuid.uuid4().hex} + self.requests.register_uri('POST', + self.TEST_URL + '/auth/tokens', + headers=headers, + json=t) + self.requests.register_uri('GET', self.TEST_URL, json={'version': d}) + + with self.deprecations.expect_deprecations_here(): + a = ksc_identity.V3Password(username=uuid.uuid4().hex, + password=uuid.uuid4().hex, + user_domain_id=uuid.uuid4().hex, + auth_url=self.TEST_URL) + + s = ksc_session.Session(auth=a) + + return v3_client.Client(session=s) + + +class KsaSessionV3(BaseV3): + + def new_client(self): + t = fixture.V3Token(user_id=self.user_id) + t.set_project_scope() + + s = t.add_service('identity') + s.add_standard_endpoints(public=self.TEST_URL, + admin=self.TEST_URL) + + d = fixture.V3Discovery(self.TEST_URL) + + headers = {'X-Subject-Token': uuid.uuid4().hex} + self.requests.register_uri('POST', + self.TEST_URL + '/auth/tokens', + headers=headers, + json=t) + self.requests.register_uri('GET', self.TEST_URL, json={'version': d}) + + a = ksa_identity.V3Password(username=uuid.uuid4().hex, + password=uuid.uuid4().hex, + user_domain_id=uuid.uuid4().hex, + auth_url=self.TEST_URL) + s = ksa_session.Session(auth=a) + return v3_client.Client(session=s) + + +def _hash_signed_token_safe(signed_text, **kwargs): + if isinstance(signed_text, str): + signed_text = signed_text.encode('utf-8') + return utils.hash_signed_token(signed_text, **kwargs) + + +class Examples(fixtures.Fixture): + """Example tokens and certs loaded from the examples directory. + + To use this class correctly, the module needs to override the test suite + class to use testresources.OptimisingTestSuite (otherwise the files will + be read on every test). This is done by defining a load_tests function + in the module, like this: + + def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) + + (see http://docs.python.org/2/library/unittest.html#load-tests-protocol ) + + """ + + def setUp(self): + super(Examples, self).setUp() + + # The data for several tests are signed using openssl and are stored in + # files in the signing subdirectory. In order to keep the values + # consistent between the tests and the signed documents, we read them + # in for use in the tests. + with open(os.path.join(CMSDIR, 'auth_token_scoped.json')) as f: + self.TOKEN_SCOPED_DATA = cms.cms_to_token(f.read()) + + with open(os.path.join(CMSDIR, 'auth_token_scoped.pem')) as f: + self.SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read()) + self.SIGNED_TOKEN_SCOPED_HASH = _hash_signed_token_safe( + self.SIGNED_TOKEN_SCOPED) + self.SIGNED_TOKEN_SCOPED_HASH_SHA256 = _hash_signed_token_safe( + self.SIGNED_TOKEN_SCOPED, mode='sha256') + with open(os.path.join(CMSDIR, 'auth_token_unscoped.pem')) as f: + self.SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_v3_token_scoped.pem')) as f: + self.SIGNED_v3_TOKEN_SCOPED = cms.cms_to_token(f.read()) + self.SIGNED_v3_TOKEN_SCOPED_HASH = _hash_signed_token_safe( + self.SIGNED_v3_TOKEN_SCOPED) + self.SIGNED_v3_TOKEN_SCOPED_HASH_SHA256 = _hash_signed_token_safe( + self.SIGNED_v3_TOKEN_SCOPED, mode='sha256') + with open(os.path.join(CMSDIR, 'auth_token_revoked.pem')) as f: + self.REVOKED_TOKEN = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_token_scoped_expired.pem')) as f: + self.SIGNED_TOKEN_SCOPED_EXPIRED = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_v3_token_revoked.pem')) as f: + self.REVOKED_v3_TOKEN = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_token_scoped.pkiz')) as f: + self.SIGNED_TOKEN_SCOPED_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_token_unscoped.pkiz')) as f: + self.SIGNED_TOKEN_UNSCOPED_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_v3_token_scoped.pkiz')) as f: + self.SIGNED_v3_TOKEN_SCOPED_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_token_revoked.pkiz')) as f: + self.REVOKED_TOKEN_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, + 'auth_token_scoped_expired.pkiz')) as f: + self.SIGNED_TOKEN_SCOPED_EXPIRED_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'auth_v3_token_revoked.pkiz')) as f: + self.REVOKED_v3_TOKEN_PKIZ = cms.cms_to_token(f.read()) + with open(os.path.join(CMSDIR, 'revocation_list.json')) as f: + self.REVOCATION_LIST = jsonutils.loads(f.read()) + with open(os.path.join(CMSDIR, 'revocation_list.pem')) as f: + self.SIGNED_REVOCATION_LIST = jsonutils.dumps({'signed': f.read()}) + + self.SIGNING_CERT_FILE = os.path.join(CERTDIR, 'signing_cert.pem') + with open(self.SIGNING_CERT_FILE) as f: + self.SIGNING_CERT = f.read() + + self.KERBEROS_BIND = 'USER@REALM' + + self.SIGNING_KEY_FILE = os.path.join(KEYDIR, 'signing_key.pem') + with open(self.SIGNING_KEY_FILE) as f: + self.SIGNING_KEY = f.read() + + self.SIGNING_CA_FILE = os.path.join(CERTDIR, 'cacert.pem') + with open(self.SIGNING_CA_FILE) as f: + self.SIGNING_CA = f.read() + + self.UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d" + self.UUID_TOKEN_NO_SERVICE_CATALOG = '8286720fbe4941e69fa8241723bb02df' + self.UUID_TOKEN_UNSCOPED = '731f903721c14827be7b2dc912af7776' + self.UUID_TOKEN_BIND = '3fc54048ad64405c98225ce0897af7c5' + self.UUID_TOKEN_UNKNOWN_BIND = '8885fdf4d42e4fb9879e6379fa1eaf48' + self.VALID_DIABLO_TOKEN = 'b0cf19b55dbb4f20a6ee18e6c6cf1726' + self.v3_UUID_TOKEN_DEFAULT = '5603457654b346fdbb93437bfe76f2f1' + self.v3_UUID_TOKEN_UNSCOPED = 'd34835fdaec447e695a0a024d84f8d79' + self.v3_UUID_TOKEN_DOMAIN_SCOPED = 'e8a7b63aaa4449f38f0c5c05c3581792' + self.v3_UUID_TOKEN_BIND = '2f61f73e1c854cbb9534c487f9bd63c2' + self.v3_UUID_TOKEN_UNKNOWN_BIND = '7ed9781b62cd4880b8d8c6788ab1d1e2' + + revoked_token = self.REVOKED_TOKEN + if isinstance(revoked_token, str): + revoked_token = revoked_token.encode('utf-8') + self.REVOKED_TOKEN_HASH = utils.hash_signed_token(revoked_token) + self.REVOKED_TOKEN_HASH_SHA256 = utils.hash_signed_token(revoked_token, + mode='sha256') + self.REVOKED_TOKEN_LIST = ( + {'revoked': [{'id': self.REVOKED_TOKEN_HASH, + 'expires': timeutils.utcnow()}]}) + self.REVOKED_TOKEN_LIST_JSON = jsonutils.dumps(self.REVOKED_TOKEN_LIST) + + revoked_v3_token = self.REVOKED_v3_TOKEN + if isinstance(revoked_v3_token, str): + revoked_v3_token = revoked_v3_token.encode('utf-8') + self.REVOKED_v3_TOKEN_HASH = utils.hash_signed_token(revoked_v3_token) + hash = utils.hash_signed_token(revoked_v3_token, mode='sha256') + self.REVOKED_v3_TOKEN_HASH_SHA256 = hash + self.REVOKED_v3_TOKEN_LIST = ( + {'revoked': [{'id': self.REVOKED_v3_TOKEN_HASH, + 'expires': timeutils.utcnow()}]}) + self.REVOKED_v3_TOKEN_LIST_JSON = jsonutils.dumps( + self.REVOKED_v3_TOKEN_LIST) + + revoked_token_pkiz = self.REVOKED_TOKEN_PKIZ + if isinstance(revoked_token_pkiz, str): + revoked_token_pkiz = revoked_token_pkiz.encode('utf-8') + self.REVOKED_TOKEN_PKIZ_HASH = utils.hash_signed_token( + revoked_token_pkiz) + revoked_v3_token_pkiz = self.REVOKED_v3_TOKEN_PKIZ + if isinstance(revoked_v3_token_pkiz, str): + revoked_v3_token_pkiz = revoked_v3_token_pkiz.encode('utf-8') + self.REVOKED_v3_PKIZ_TOKEN_HASH = utils.hash_signed_token( + revoked_v3_token_pkiz) + + self.REVOKED_TOKEN_PKIZ_LIST = ( + {'revoked': [{'id': self.REVOKED_TOKEN_PKIZ_HASH, + 'expires': timeutils.utcnow()}, + {'id': self.REVOKED_v3_PKIZ_TOKEN_HASH, + 'expires': timeutils.utcnow()}, + ]}) + self.REVOKED_TOKEN_PKIZ_LIST_JSON = jsonutils.dumps( + self.REVOKED_TOKEN_PKIZ_LIST) + + self.SIGNED_TOKEN_SCOPED_KEY = cms.cms_hash_token( + self.SIGNED_TOKEN_SCOPED) + self.SIGNED_TOKEN_UNSCOPED_KEY = cms.cms_hash_token( + self.SIGNED_TOKEN_UNSCOPED) + self.SIGNED_v3_TOKEN_SCOPED_KEY = cms.cms_hash_token( + self.SIGNED_v3_TOKEN_SCOPED) + + self.SIGNED_TOKEN_SCOPED_PKIZ_KEY = cms.cms_hash_token( + self.SIGNED_TOKEN_SCOPED_PKIZ) + self.SIGNED_TOKEN_UNSCOPED_PKIZ_KEY = cms.cms_hash_token( + self.SIGNED_TOKEN_UNSCOPED_PKIZ) + self.SIGNED_v3_TOKEN_SCOPED_PKIZ_KEY = cms.cms_hash_token( + self.SIGNED_v3_TOKEN_SCOPED_PKIZ) + + self.INVALID_SIGNED_TOKEN = ( + "MIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE" + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + "0000000000000000000000000000000000000000000000000000000000000000" + "1111111111111111111111111111111111111111111111111111111111111111" + "2222222222222222222222222222222222222222222222222222222222222222" + "3333333333333333333333333333333333333333333333333333333333333333" + "4444444444444444444444444444444444444444444444444444444444444444" + "5555555555555555555555555555555555555555555555555555555555555555" + "6666666666666666666666666666666666666666666666666666666666666666" + "7777777777777777777777777777777777777777777777777777777777777777" + "8888888888888888888888888888888888888888888888888888888888888888" + "9999999999999999999999999999999999999999999999999999999999999999" + "0000000000000000000000000000000000000000000000000000000000000000") + + self.INVALID_SIGNED_PKIZ_TOKEN = ( + "PKIZ_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE" + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + "0000000000000000000000000000000000000000000000000000000000000000" + "1111111111111111111111111111111111111111111111111111111111111111" + "2222222222222222222222222222222222222222222222222222222222222222" + "3333333333333333333333333333333333333333333333333333333333333333" + "4444444444444444444444444444444444444444444444444444444444444444" + "5555555555555555555555555555555555555555555555555555555555555555" + "6666666666666666666666666666666666666666666666666666666666666666" + "7777777777777777777777777777777777777777777777777777777777777777" + "8888888888888888888888888888888888888888888888888888888888888888" + "9999999999999999999999999999999999999999999999999999999999999999" + "0000000000000000000000000000000000000000000000000000000000000000") + + # JSON responses keyed by token ID + self.TOKEN_RESPONSES = { + self.UUID_TOKEN_DEFAULT: { + 'access': { + 'token': { + 'id': self.UUID_TOKEN_DEFAULT, + 'expires': '2999-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + }, + self.VALID_DIABLO_TOKEN: { + 'access': { + 'token': { + 'id': self.VALID_DIABLO_TOKEN, + 'expires': '2999-01-01T00:00:10.000123Z', + 'tenantId': 'tenant_id1', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + self.UUID_TOKEN_UNSCOPED: { + 'access': { + 'token': { + 'id': self.UUID_TOKEN_UNSCOPED, + 'expires': '2999-01-01T00:00:10.000123Z', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + self.UUID_TOKEN_NO_SERVICE_CATALOG: { + 'access': { + 'token': { + 'id': 'valid-token', + 'expires': '2999-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + } + }, + }, + self.UUID_TOKEN_BIND: { + 'access': { + 'token': { + 'bind': {'kerberos': self.KERBEROS_BIND}, + 'id': self.UUID_TOKEN_BIND, + 'expires': '2999-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + }, + self.UUID_TOKEN_UNKNOWN_BIND: { + 'access': { + 'token': { + 'bind': {'FOO': 'BAR'}, + 'id': self.UUID_TOKEN_UNKNOWN_BIND, + 'expires': '2999-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + }, + self.v3_UUID_TOKEN_DEFAULT: { + 'token': { + 'expires_at': '2999-01-01T00:00:10.000123Z', + 'methods': ['password'], + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + self.v3_UUID_TOKEN_UNSCOPED: { + 'token': { + 'expires_at': '2999-01-01T00:00:10.000123Z', + 'methods': ['password'], + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + } + } + }, + self.v3_UUID_TOKEN_DOMAIN_SCOPED: { + 'token': { + 'expires_at': '2999-01-01T00:00:10.000123Z', + 'methods': ['password'], + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1', + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + self.SIGNED_TOKEN_SCOPED_KEY: { + 'access': { + 'token': { + 'id': self.SIGNED_TOKEN_SCOPED_KEY, + 'expires': '2999-01-01T00:00:10.000123Z', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'tenantId': 'tenant_id1', + 'tenantName': 'tenant_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + self.SIGNED_TOKEN_UNSCOPED_KEY: { + 'access': { + 'token': { + 'id': self.SIGNED_TOKEN_UNSCOPED_KEY, + 'expires': '2999-01-01T00:00:10.000123Z', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + self.SIGNED_v3_TOKEN_SCOPED_KEY: { + 'token': { + 'expires_at': '2999-01-01T00:00:10.000123Z', + 'methods': ['password'], + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'} + ], + 'catalog': {} + } + }, + self.v3_UUID_TOKEN_BIND: { + 'token': { + 'bind': {'kerberos': self.KERBEROS_BIND}, + 'methods': ['password'], + 'expires_at': '2999-01-01T00:00:10.000123Z', + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + self.v3_UUID_TOKEN_UNKNOWN_BIND: { + 'token': { + 'bind': {'FOO': 'BAR'}, + 'expires_at': '2999-01-01T00:00:10.000123Z', + 'methods': ['password'], + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + } + self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_PKIZ_KEY] = ( + self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_KEY]) + self.TOKEN_RESPONSES[self.SIGNED_TOKEN_UNSCOPED_PKIZ_KEY] = ( + self.TOKEN_RESPONSES[self.SIGNED_TOKEN_UNSCOPED_KEY]) + self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_PKIZ_KEY] = ( + self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_KEY]) + + self.JSON_TOKEN_RESPONSES = dict([(k, jsonutils.dumps(v)) for k, v in + self.TOKEN_RESPONSES.items()]) + + +EXAMPLES_RESOURCE = testresources.FixtureResource(Examples()) + + +class Deprecations(fixtures.Fixture): + def setUp(self): + super(Deprecations, self).setUp() + + # If keystoneclient calls any deprecated function this will raise an + # exception. + warnings.filterwarnings('error', category=DeprecationWarning, + module='^keystoneclient\\.') + warnings.filterwarnings('ignore', category=DeprecationWarning, + module='^debtcollector\\.') + self.addCleanup(warnings.resetwarnings) + + def expect_deprecations(self): + """Call this if the test expects to call deprecated function.""" + warnings.resetwarnings() + warnings.filterwarnings('ignore', category=DeprecationWarning, + module='^keystoneclient\\.') + warnings.filterwarnings('ignore', category=DeprecationWarning, + module='^debtcollector\\.') + + @contextlib.contextmanager + def expect_deprecations_here(self): + warnings.resetwarnings() + warnings.filterwarnings('ignore', category=DeprecationWarning, + module='^keystoneclient\\.') + warnings.filterwarnings('ignore', category=DeprecationWarning, + module='^debtcollector\\.') + yield + warnings.resetwarnings() + warnings.filterwarnings('error', category=DeprecationWarning, + module='^keystoneclient\\.') + warnings.filterwarnings('ignore', category=DeprecationWarning, + module='^debtcollector\\.') diff --git a/keystoneclient/tests/unit/generic/__init__.py b/keystoneclient/tests/unit/generic/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/keystoneclient/tests/unit/generic/test_client.py b/keystoneclient/tests/unit/generic/test_client.py new file mode 100644 index 000000000..5c27b6eb9 --- /dev/null +++ b/keystoneclient/tests/unit/generic/test_client.py @@ -0,0 +1,68 @@ +# Copyright 2014 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. + +from oslo_serialization import jsonutils + +from keystoneclient.generic import client +from keystoneclient.tests.unit import utils + +BASE_HOST = 'http://keystone.example.com' +BASE_URL = "%s:5000/" % BASE_HOST +V2_URL = "%sv2.0" % BASE_URL + +EXTENSION_NAMESPACE = ("https://docs.openstack.org/identity/api/ext/OS-FAKE/" + "v1.0") +EXTENSION_DESCRIBED = {"href": "https://github.com/openstack/identity-api", + "rel": "describedby", + "type": "text/html"} + +EXTENSION_ALIAS_FOO = "OS-FAKE-FOO" +EXTENSION_NAME_FOO = "OpenStack Keystone Fake Extension Foo" +EXTENSION_FOO = {"alias": EXTENSION_ALIAS_FOO, + "description": "Fake Foo extension to V2.0 API.", + "links": [EXTENSION_DESCRIBED], + "name": EXTENSION_NAME_FOO, + "namespace": EXTENSION_NAMESPACE, + "updated": '2014-01-08T00:00:00Z'} + +EXTENSION_ALIAS_BAR = "OS-FAKE-BAR" +EXTENSION_NAME_BAR = "OpenStack Keystone Fake Extension Bar" +EXTENSION_BAR = {"alias": EXTENSION_ALIAS_BAR, + "description": "Fake Bar extension to V2.0 API.", + "links": [EXTENSION_DESCRIBED], + "name": EXTENSION_NAME_BAR, + "namespace": EXTENSION_NAMESPACE, + "updated": '2014-01-08T00:00:00Z'} + + +def _create_extension_list(extensions): + return jsonutils.dumps({'extensions': {'values': extensions}}) + + +EXTENSION_LIST = _create_extension_list([EXTENSION_FOO, EXTENSION_BAR]) + + +class ClientDiscoveryTests(utils.TestCase): + + def test_discover_extensions_v2(self): + self.requests_mock.get("%s/extensions" % V2_URL, text=EXTENSION_LIST) + # Creating a HTTPClient not using session is deprecated. + # creating a generic client at all is deprecated. + with self.deprecations.expect_deprecations_here(): + extensions = client.Client().discover_extensions(url=V2_URL) + self.assertIn(EXTENSION_ALIAS_FOO, extensions) + self.assertEqual(extensions[EXTENSION_ALIAS_FOO], EXTENSION_NAME_FOO) + self.assertIn(EXTENSION_ALIAS_BAR, extensions) + self.assertEqual(extensions[EXTENSION_ALIAS_BAR], EXTENSION_NAME_BAR) diff --git a/keystoneclient/tests/unit/test_base.py b/keystoneclient/tests/unit/test_base.py new file mode 100644 index 000000000..ca57dc469 --- /dev/null +++ b/keystoneclient/tests/unit/test_base.py @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +import fixtures +from keystoneauth1.identity import v2 +from keystoneauth1 import session +import requests + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.tests.unit import utils +from keystoneclient import utils as base_utils +from keystoneclient.v2_0 import client +from keystoneclient.v2_0 import roles +from keystoneclient.v3 import users + +TEST_REQUEST_ID = uuid.uuid4().hex +TEST_REQUEST_ID_1 = uuid.uuid4().hex + + +def create_response_with_request_id_header(): + resp = requests.Response() + resp.headers['x-openstack-request-id'] = TEST_REQUEST_ID + return resp + + +class HumanReadable(base.Resource): + HUMAN_ID = True + + +class BaseTest(utils.TestCase): + + def test_resource_repr(self): + r = base.Resource(None, dict(foo="bar", baz="spam")) + self.assertEqual(repr(r), "") + + def test_getid(self): + self.assertEqual(base.getid(4), 4) + + class TmpObject(object): + id = 4 + self.assertEqual(base.getid(TmpObject), 4) + + def test_resource_lazy_getattr(self): + auth = v2.Token(token=self.TEST_TOKEN, + auth_url='http://127.0.0.1:5000') + session_ = session.Session(auth=auth) + self.client = client.Client(session=session_) + + self.useFixture(fixtures.MockPatchObject( + self.client._adapter, 'get', side_effect=AttributeError, + autospec=True)) + + f = roles.Role(self.client.roles, {'id': 1, 'name': 'Member'}) + self.assertEqual(f.name, 'Member') + + # Missing stuff still fails after a second get + self.assertRaises(AttributeError, getattr, f, 'blahblah') + + def test_eq(self): + # Two resources with same ID: never equal if their info is not equal + r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) + r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) + self.assertNotEqual(r1, r2) + self.assertTrue(r1 != r2) + + # Two resources with same ID: equal if their info is equal + # The truth of r1==r2 does not imply that r1!=r2 is false in PY2. + # Test that inequality operator is defined and that comparing equal + # items returns False + r1 = base.Resource(None, {'id': 1, 'name': 'hello'}) + r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) + self.assertTrue(r1 == r2) + self.assertFalse(r1 != r2) + + # Two resources of different types: never equal + r1 = base.Resource(None, {'id': 1}) + r2 = roles.Role(None, {'id': 1}) + self.assertNotEqual(r1, r2) + self.assertTrue(r1 != r2) + + # Two resources with no ID: equal if their info is equal + # The truth of r1==r2 does not imply that r1!=r2 is false in PY2. + # Test that inequality operator is defined and that comparing equal + # items returns False. + r1 = base.Resource(None, {'name': 'joe', 'age': 12}) + r2 = base.Resource(None, {'name': 'joe', 'age': 12}) + self.assertTrue(r1 == r2) + self.assertFalse(r1 != r2) + + r1 = base.Resource(None, {'id': 1}) + self.assertNotEqual(r1, object()) + self.assertTrue(r1 != object()) + self.assertNotEqual(r1, {'id': 1}) + self.assertTrue(r1 != {'id': 1}) + + def test_human_id(self): + r = base.Resource(None, {"name": "1 of !"}) + self.assertIsNone(r.human_id) + r = HumanReadable(None, {"name": "1 of !"}) + self.assertEqual(r.human_id, "1-of") + + def test_non_ascii_attr(self): + r_dict = {"name": "foobar", + u"тест": "1234", + u"тест2": u"привет мир"} + + r = base.Resource(None, r_dict) + self.assertEqual(r.name, "foobar") + self.assertEqual(r.to_dict(), r_dict) + + +class ManagerTest(utils.TestCase): + body = {"hello": {"hi": 1}} + url = "/test-url" + + def setUp(self): + super(ManagerTest, self).setUp() + + auth = v2.Token(auth_url='http://127.0.0.1:5000', + token=self.TEST_TOKEN) + session_ = session.Session(auth=auth) + self.client = client.Client(session=session_)._adapter + + self.mgr = base.Manager(self.client) + self.mgr.resource_class = base.Resource + + def test_api(self): + with self.deprecations.expect_deprecations_here(): + self.assertEqual(self.mgr.api, self.client) + + def test_get(self): + get_mock = self.useFixture(fixtures.MockPatchObject( + self.client, 'get', autospec=True, return_value=(None, self.body)) + ).mock + rsrc = self.mgr._get(self.url, "hello") + get_mock.assert_called_once_with(self.url) + self.assertEqual(rsrc.hi, 1) + + def test_post(self): + post_mock = self.useFixture(fixtures.MockPatchObject( + self.client, 'post', autospec=True, return_value=(None, self.body)) + ).mock + + rsrc = self.mgr._post(self.url, self.body, "hello") + post_mock.assert_called_once_with(self.url, body=self.body) + self.assertEqual(rsrc.hi, 1) + + post_mock.reset_mock() + + rsrc = self.mgr._post(self.url, self.body, "hello", return_raw=True) + post_mock.assert_called_once_with(self.url, body=self.body) + self.assertEqual(rsrc["hi"], 1) + + def test_put(self): + put_mock = self.useFixture(fixtures.MockPatchObject( + self.client, 'put', autospec=True, return_value=(None, self.body)) + ).mock + + rsrc = self.mgr._put(self.url, self.body, "hello") + put_mock.assert_called_once_with(self.url, body=self.body) + self.assertEqual(rsrc.hi, 1) + + put_mock.reset_mock() + + rsrc = self.mgr._put(self.url, self.body) + put_mock.assert_called_once_with(self.url, body=self.body) + self.assertEqual(rsrc.hello["hi"], 1) + + def test_patch(self): + patch_mock = self.useFixture(fixtures.MockPatchObject( + self.client, 'patch', autospec=True, + return_value=(None, self.body)) + ).mock + + rsrc = self.mgr._patch(self.url, self.body, "hello") + patch_mock.assert_called_once_with(self.url, body=self.body) + self.assertEqual(rsrc.hi, 1) + + patch_mock.reset_mock() + + rsrc = self.mgr._patch(self.url, self.body) + patch_mock.assert_called_once_with(self.url, body=self.body) + self.assertEqual(rsrc.hello["hi"], 1) + + def test_update(self): + patch_mock = self.useFixture(fixtures.MockPatchObject( + self.client, 'patch', autospec=True, + return_value=(None, self.body)) + ).mock + + put_mock = self.useFixture(fixtures.MockPatchObject( + self.client, 'put', autospec=True, return_value=(None, self.body)) + ).mock + + rsrc = self.mgr._update( + self.url, body=self.body, response_key="hello", method="PATCH", + management=False) + patch_mock.assert_called_once_with( + self.url, management=False, body=self.body) + self.assertEqual(rsrc.hi, 1) + + rsrc = self.mgr._update( + self.url, body=None, response_key="hello", method="PUT", + management=True) + put_mock.assert_called_once_with(self.url, management=True, body=None) + self.assertEqual(rsrc.hi, 1) + + +class ManagerRequestIdTest(utils.TestCase): + url = "/test-url" + resp = create_response_with_request_id_header() + + def setUp(self): + super(ManagerRequestIdTest, self).setUp() + + auth = v2.Token(auth_url='http://127.0.0.1:5000', + token=self.TEST_TOKEN) + session_ = session.Session(auth=auth) + self.client = client.Client(session=session_, + include_metadata='True')._adapter + + self.mgr = base.Manager(self.client) + self.mgr.resource_class = base.Resource + + def mock_request_method(self, request_method, body): + return self.useFixture(fixtures.MockPatchObject( + self.client, request_method, autospec=True, + return_value=(self.resp, body)) + ).mock + + def test_get(self): + body = {"hello": {"hi": 1}} + get_mock = self.mock_request_method('get', body) + rsrc = self.mgr._get(self.url, "hello") + get_mock.assert_called_once_with(self.url) + self.assertEqual(rsrc.data.hi, 1) + self.assertEqual(rsrc.request_ids[0], TEST_REQUEST_ID) + + def test_list(self): + body = {"hello": [{"name": "admin"}, {"name": "admin"}]} + get_mock = self.mock_request_method('get', body) + + returned_list = self.mgr._list(self.url, "hello") + self.assertEqual(returned_list.request_ids[0], TEST_REQUEST_ID) + get_mock.assert_called_once_with(self.url) + + def test_list_with_multiple_response_objects(self): + body = {"hello": [{"name": "admin"}, {"name": "admin"}]} + resp_1 = requests.Response() + resp_1.headers['x-openstack-request-id'] = TEST_REQUEST_ID + resp_2 = requests.Response() + resp_2.headers['x-openstack-request-id'] = TEST_REQUEST_ID_1 + + resp_result = [resp_1, resp_2] + get_mock = self.useFixture(fixtures.MockPatchObject( + self.client, 'get', autospec=True, + return_value=(resp_result, body)) + ).mock + + returned_list = self.mgr._list(self.url, "hello") + self.assertIn(returned_list.request_ids[0], [ + TEST_REQUEST_ID, TEST_REQUEST_ID_1]) + self.assertIn(returned_list.request_ids[1], [ + TEST_REQUEST_ID, TEST_REQUEST_ID_1]) + get_mock.assert_called_once_with(self.url) + + def test_post(self): + body = {"hello": {"hi": 1}} + post_mock = self.mock_request_method('post', body) + rsrc = self.mgr._post(self.url, body, "hello") + post_mock.assert_called_once_with(self.url, body=body) + self.assertEqual(rsrc.data.hi, 1) + + post_mock.reset_mock() + + rsrc = self.mgr._post(self.url, body, "hello", return_raw=True) + post_mock.assert_called_once_with(self.url, body=body) + self.assertNotIsInstance(rsrc, base.Response) + self.assertEqual(rsrc["hi"], 1) + + def test_put(self): + body = {"hello": {"hi": 1}} + put_mock = self.mock_request_method('put', body) + rsrc = self.mgr._put(self.url, body, "hello") + put_mock.assert_called_once_with(self.url, body=body) + self.assertEqual(rsrc.data.hi, 1) + + put_mock.reset_mock() + + rsrc = self.mgr._put(self.url, body) + put_mock.assert_called_once_with(self.url, body=body) + self.assertEqual(rsrc.data.hello["hi"], 1) + self.assertEqual(rsrc.request_ids[0], TEST_REQUEST_ID) + + def test_head(self): + get_mock = self.mock_request_method('head', None) + rsrc = self.mgr._head(self.url) + get_mock.assert_called_once_with(self.url) + self.assertFalse(rsrc.data) + self.assertEqual(rsrc.request_ids[0], TEST_REQUEST_ID) + + def test_delete(self): + delete_mock = self.mock_request_method('delete', None) + resp, base_resp = self.mgr._delete(self.url, name="hello") + + delete_mock.assert_called_once_with('/test-url', name='hello') + self.assertEqual(base_resp.request_ids[0], TEST_REQUEST_ID) + self.assertEqual(base_resp.data, None) + self.assertIsInstance(resp, requests.Response) + + def test_patch(self): + body = {"hello": {"hi": 1}} + patch_mock = self.mock_request_method('patch', body) + rsrc = self.mgr._patch(self.url, body, "hello") + patch_mock.assert_called_once_with(self.url, body=body) + self.assertEqual(rsrc.data.hi, 1) + + patch_mock.reset_mock() + + rsrc = self.mgr._patch(self.url, body) + patch_mock.assert_called_once_with(self.url, body=body) + self.assertEqual(rsrc.data.hello["hi"], 1) + self.assertEqual(rsrc.request_ids[0], TEST_REQUEST_ID) + + def test_update(self): + body = {"hello": {"hi": 1}} + patch_mock = self.mock_request_method('patch', body) + put_mock = self.mock_request_method('put', body) + + rsrc = self.mgr._update( + self.url, body=body, response_key="hello", method="PATCH", + management=False) + patch_mock.assert_called_once_with( + self.url, management=False, body=body) + self.assertEqual(rsrc.data.hi, 1) + + rsrc = self.mgr._update( + self.url, body=None, response_key="hello", method="PUT", + management=True) + put_mock.assert_called_once_with(self.url, management=True, body=None) + self.assertEqual(rsrc.data.hi, 1) + self.assertEqual(rsrc.request_ids[0], TEST_REQUEST_ID) + + +class ManagerWithFindRequestIdTest(utils.TestCase): + url = "/fakes" + resp = create_response_with_request_id_header() + + def setUp(self): + super(ManagerWithFindRequestIdTest, self).setUp() + + auth = v2.Token(auth_url='http://127.0.0.1:5000', + token=self.TEST_TOKEN) + session_ = session.Session(auth=auth) + self.client = client.Client(session=session_, + include_metadata='True')._adapter + + def test_find_resource(self): + body = {"roles": [{"name": 'entity_one'}, {"name": 'entity_one_1'}]} + request_resp = requests.Response() + request_resp.headers['x-openstack-request-id'] = TEST_REQUEST_ID + + get_mock = self.useFixture(fixtures.MockPatchObject( + self.client, 'get', autospec=True, + side_effect=[exceptions.NotFound, (request_resp, body)]) + ).mock + + mgr = roles.RoleManager(self.client) + mgr.resource_class = roles.Role + response = base_utils.find_resource(mgr, 'entity_one') + get_mock.assert_called_with('/OS-KSADM/roles') + self.assertEqual(response.request_ids[0], TEST_REQUEST_ID) + + +class CrudManagerRequestIdTest(utils.TestCase): + resp = create_response_with_request_id_header() + request_resp = requests.Response() + request_resp.headers['x-openstack-request-id'] = TEST_REQUEST_ID + + def setUp(self): + super(CrudManagerRequestIdTest, self).setUp() + + auth = v2.Token(auth_url='http://127.0.0.1:5000', + token=self.TEST_TOKEN) + session_ = session.Session(auth=auth) + self.client = client.Client(session=session_, + include_metadata='True')._adapter + + def test_find_resource(self): + body = {"users": [{"name": 'entity_one'}]} + get_mock = self.useFixture(fixtures.MockPatchObject( + self.client, 'get', autospec=True, + side_effect=[exceptions.NotFound, (self.request_resp, body)]) + ).mock + mgr = users.UserManager(self.client) + mgr.resource_class = users.User + response = base_utils.find_resource(mgr, 'entity_one') + get_mock.assert_called_with('/users?name=entity_one') + self.assertEqual(response.request_ids[0], TEST_REQUEST_ID) + + def test_list(self): + body = {"users": [{"name": "admin"}, {"name": "admin"}]} + + get_mock = self.useFixture(fixtures.MockPatchObject( + self.client, 'get', autospec=True, + return_value=(self.request_resp, body)) + ).mock + mgr = users.UserManager(self.client) + mgr.resource_class = users.User + returned_list = mgr.list() + self.assertEqual(returned_list.request_ids[0], TEST_REQUEST_ID) + get_mock.assert_called_once_with('/users?') diff --git a/keystoneclient/tests/unit/test_cms.py b/keystoneclient/tests/unit/test_cms.py new file mode 100644 index 000000000..2671bcb49 --- /dev/null +++ b/keystoneclient/tests/unit/test_cms.py @@ -0,0 +1,201 @@ +# 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 errno +import os +import subprocess +from unittest import mock + +import testresources +from testtools import matchers + +from keystoneclient.common import cms +from keystoneclient import exceptions +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit import utils + + +class CMSTest(utils.TestCase, testresources.ResourcedTestCase): + """Unit tests for the keystoneclient.common.cms module.""" + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def __init__(self, *args, **kwargs): + super(CMSTest, self).__init__(*args, **kwargs) + process = subprocess.Popen(['openssl', 'version'], + stdout=subprocess.PIPE) + out, err = process.communicate() + # Example output: 'OpenSSL 0.9.8za 5 Jun 2014' + openssl_version = out.split()[1] + + if err or openssl_version.startswith(b'0'): + raise Exception('Your version of OpenSSL is not supported. ' + 'You will need to update it to 1.0 or later.') + + def _raise_OSError(*args): + e = OSError() + e.errno = errno.EPIPE + raise e + + def test_cms_verify(self): + self.assertRaises(exceptions.CertificateConfigError, + cms.cms_verify, + 'data', + 'no_exist_cert_file', + 'no_exist_ca_file') + + def test_token_tocms_to_token(self): + with open(os.path.join(client_fixtures.CMSDIR, + 'auth_token_scoped.pem')) as f: + AUTH_TOKEN_SCOPED_CMS = f.read() + + self.assertEqual(cms.token_to_cms(self.examples.SIGNED_TOKEN_SCOPED), + AUTH_TOKEN_SCOPED_CMS) + + tok = cms.cms_to_token(cms.token_to_cms( + self.examples.SIGNED_TOKEN_SCOPED)) + self.assertEqual(tok, self.examples.SIGNED_TOKEN_SCOPED) + + def test_asn1_token(self): + self.assertTrue(cms.is_asn1_token(self.examples.SIGNED_TOKEN_SCOPED)) + self.assertFalse(cms.is_asn1_token('FOOBAR')) + + def test_cms_sign_token_no_files(self): + self.assertRaises(subprocess.CalledProcessError, + cms.cms_sign_token, + self.examples.TOKEN_SCOPED_DATA, + '/no/such/file', '/no/such/key') + + def test_cms_sign_token_no_files_pkiz(self): + self.assertRaises(subprocess.CalledProcessError, + cms.pkiz_sign, + self.examples.TOKEN_SCOPED_DATA, + '/no/such/file', '/no/such/key') + + def test_cms_sign_token_success(self): + self.assertTrue( + cms.pkiz_sign(self.examples.TOKEN_SCOPED_DATA, + self.examples.SIGNING_CERT_FILE, + self.examples.SIGNING_KEY_FILE)) + + def test_cms_verify_token_no_files(self): + self.assertRaises(exceptions.CertificateConfigError, + cms.cms_verify, + self.examples.SIGNED_TOKEN_SCOPED, + '/no/such/file', '/no/such/key') + + def test_cms_verify_token_no_oserror(self): + with mock.patch('subprocess.Popen.communicate', + new=self._raise_OSError): + try: + cms.cms_verify("x", '/no/such/file', '/no/such/key') + except exceptions.CertificateConfigError as e: + self.assertIn('/no/such/file', e.output) + self.assertIn('Hit OSError ', e.output) + else: + self.fail('Expected exceptions.CertificateConfigError') + + def test_cms_verify_token_scoped(self): + cms_content = cms.token_to_cms(self.examples.SIGNED_TOKEN_SCOPED) + self.assertTrue(cms.cms_verify(cms_content, + self.examples.SIGNING_CERT_FILE, + self.examples.SIGNING_CA_FILE)) + + def test_cms_verify_token_scoped_expired(self): + cms_content = cms.token_to_cms( + self.examples.SIGNED_TOKEN_SCOPED_EXPIRED) + self.assertTrue(cms.cms_verify(cms_content, + self.examples.SIGNING_CERT_FILE, + self.examples.SIGNING_CA_FILE)) + + def test_cms_verify_token_unscoped(self): + cms_content = cms.token_to_cms(self.examples.SIGNED_TOKEN_UNSCOPED) + self.assertTrue(cms.cms_verify(cms_content, + self.examples.SIGNING_CERT_FILE, + self.examples.SIGNING_CA_FILE)) + + def test_cms_verify_token_v3_scoped(self): + cms_content = cms.token_to_cms(self.examples.SIGNED_v3_TOKEN_SCOPED) + self.assertTrue(cms.cms_verify(cms_content, + self.examples.SIGNING_CERT_FILE, + self.examples.SIGNING_CA_FILE)) + + def test_cms_hash_token_no_token_id(self): + token_id = None + self.assertThat(cms.cms_hash_token(token_id), matchers.Is(None)) + + def test_cms_hash_token_not_pki(self): + """If the token_id is not a PKI token then it returns the token_id.""" + token = 'something' + self.assertFalse(cms.is_asn1_token(token)) + self.assertThat(cms.cms_hash_token(token), matchers.Is(token)) + + def test_cms_hash_token_default_md5(self): + """The default hash method is md5.""" + token = self.examples.SIGNED_TOKEN_SCOPED + token_id_default = cms.cms_hash_token(token) + token_id_md5 = cms.cms_hash_token(token, mode='md5') + self.assertThat(token_id_default, matchers.Equals(token_id_md5)) + # md5 hash is 32 chars. + self.assertThat(token_id_default, matchers.HasLength(32)) + + def test_cms_hash_token_sha256(self): + """Can also hash with sha256.""" + token = self.examples.SIGNED_TOKEN_SCOPED + token_id = cms.cms_hash_token(token, mode='sha256') + # sha256 hash is 64 chars. + self.assertThat(token_id, matchers.HasLength(64)) + + @mock.patch('keystoneclient.common.cms._check_files_accessible') + def test_process_communicate_handle_oserror_epipe(self, files_acc_mock): + process_mock = mock.Mock() + process_mock.communicate = self._raise_OSError + process_mock.stderr = mock.Mock() + process_mock.stderr.read = mock.Mock(return_value='proc stderr') + files_acc_mock.return_value = 1, ('file_path', 'fileerror') + output, err, retcode = cms._process_communicate_handle_oserror( + process_mock, '', []) + + self.assertEqual((output, retcode), ('', 1)) + self.assertIn('file_path', err) + self.assertIn('fileerror', err) + self.assertIn('proc stderr', err) + + @mock.patch('keystoneclient.common.cms._check_files_accessible') + def test_process_communicate_handle_oserror_epipe_files_ok( + self, files_acc_mock): + process_mock = mock.Mock() + process_mock.communicate = self._raise_OSError + process_mock.stderr = mock.Mock() + process_mock.stderr.read = mock.Mock(return_value='proc stderr') + files_acc_mock.return_value = -1, None + output, err, retcode = cms._process_communicate_handle_oserror( + process_mock, '', []) + + self.assertEqual((output, retcode), ('', -1)) + self.assertIn('proc stderr', err) + + def test_process_communicate_handle_oserror_no_exception(self): + process_mock = mock.Mock() + process_mock.communicate.return_value = 'out', 'err' + process_mock.poll.return_value = 0 + + output, err, retcode = cms._process_communicate_handle_oserror( + process_mock, '', []) + + self.assertEqual(output, 'out') + self.assertEqual(err, 'err') + self.assertEqual(retcode, 0) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/test_discovery.py b/keystoneclient/tests/unit/test_discovery.py new file mode 100644 index 000000000..57c7002cc --- /dev/null +++ b/keystoneclient/tests/unit/test_discovery.py @@ -0,0 +1,837 @@ +# 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 re +import uuid + +from keystoneauth1 import fixture +from oslo_serialization import jsonutils +from testtools import matchers + +from keystoneclient import _discover +from keystoneclient.auth import token_endpoint +from keystoneclient import client +from keystoneclient import discover +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit import utils +from keystoneclient.v2_0 import client as v2_client +from keystoneclient.v3 import client as v3_client + + +BASE_HOST = 'http://keystone.example.com' +BASE_URL = "%s:5000/" % BASE_HOST +UPDATED = '2013-03-06T00:00:00Z' + +TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "adminURL": "%s:8774/v1.0" % BASE_HOST, + "region": "RegionOne", + "internalURL": "%s://127.0.0.1:8774/v1.0" % BASE_HOST, + "publicURL": "%s:8774/v1.0/" % BASE_HOST + }], + "type": "nova_compat", + "name": "nova_compat" +}, { + "endpoints": [{ + "adminURL": "http://nova/novapi/admin", + "region": "RegionOne", + "internalURL": "http://nova/novapi/internal", + "publicURL": "http://nova/novapi/public" + }], + "type": "compute", + "name": "nova" +}, { + "endpoints": [{ + "adminURL": "http://glance/glanceapi/admin", + "region": "RegionOne", + "internalURL": "http://glance/glanceapi/internal", + "publicURL": "http://glance/glanceapi/public" + }], + "type": "image", + "name": "glance" +}, { + "endpoints": [{ + "adminURL": "%s:35357/v2.0" % BASE_HOST, + "region": "RegionOne", + "internalURL": "%s:5000/v2.0" % BASE_HOST, + "publicURL": "%s:5000/v2.0" % BASE_HOST + }], + "type": "identity", + "name": "keystone" +}, { + "endpoints": [{ + "adminURL": "http://swift/swiftapi/admin", + "region": "RegionOne", + "internalURL": "http://swift/swiftapi/internal", + "publicURL": "http://swift/swiftapi/public" + }], + "type": "object-store", + "name": "swift" +}] + +V2_URL = "%sv2.0" % BASE_URL +V2_VERSION = fixture.V2Discovery(V2_URL) +V2_VERSION.updated_str = UPDATED + +V2_AUTH_RESPONSE = jsonutils.dumps({ + "access": { + "token": { + "expires": "2999-01-01T00:00:10.000123Z", + "id": 'fakeToken', + "tenant": { + "id": '1' + }, + }, + "user": { + "id": 'test' + }, + "serviceCatalog": TEST_SERVICE_CATALOG, + }, +}) + +V3_URL = "%sv3" % BASE_URL +V3_VERSION = fixture.V3Discovery(V3_URL) +V3_MEDIA_TYPES = V3_VERSION.media_types +V3_VERSION.updated_str = UPDATED + +V3_TOKEN = ('3e2813b7ba0b4006840c3825860b86ed',) +V3_AUTH_RESPONSE = jsonutils.dumps({ + "token": { + "methods": [ + "token", + "password" + ], + + "expires_at": "2999-01-01T00:00:10.000123Z", + "project": { + "domain": { + "id": '1', + "name": 'test-domain' + }, + "id": '1', + "name": 'test-project' + }, + "user": { + "domain": { + "id": '1', + "name": 'test-domain' + }, + "id": '1', + "name": 'test-user' + }, + "issued_at": "2013-05-29T16:55:21.468960Z", + }, +}) + +CINDER_EXAMPLES = { + "versions": [ + { + "status": "CURRENT", + "updated": "2012-01-04T11:33:21Z", + "id": "v1.0", + "links": [ + { + "href": "%sv1/" % BASE_URL, + "rel": "self" + } + ] + }, + { + "status": "CURRENT", + "updated": "2012-11-21T11:33:21Z", + "id": "v2.0", + "links": [ + { + "href": "%sv2/" % BASE_URL, + "rel": "self" + } + ] + } + ] +} + +GLANCE_EXAMPLES = { + "versions": [ + { + "status": "CURRENT", + "id": "v2.2", + "links": [ + { + "href": "%sv2/" % BASE_URL, + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.1", + "links": [ + { + "href": "%sv2/" % BASE_URL, + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.0", + "links": [ + { + "href": "%sv2/" % BASE_URL, + "rel": "self" + } + ] + }, + { + "status": "CURRENT", + "id": "v1.1", + "links": [ + { + "href": "%sv1/" % BASE_URL, + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v1.0", + "links": [ + { + "href": "%sv1/" % BASE_URL, + "rel": "self" + } + ] + } + ] +} + + +def _create_version_list(versions): + return jsonutils.dumps({'versions': {'values': versions}}) + + +def _create_single_version(version): + return jsonutils.dumps({'version': version}) + + +V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION]) +V2_VERSION_LIST = _create_version_list([V2_VERSION]) + +V3_VERSION_ENTRY = _create_single_version(V3_VERSION) +V2_VERSION_ENTRY = _create_single_version(V2_VERSION) + + +class AvailableVersionsTests(utils.TestCase): + + def setUp(self): + super(AvailableVersionsTests, self).setUp() + self.deprecations.expect_deprecations() + + def test_available_versions_basics(self): + examples = {'keystone': V3_VERSION_LIST, + 'cinder': jsonutils.dumps(CINDER_EXAMPLES), + 'glance': jsonutils.dumps(GLANCE_EXAMPLES)} + + for path, text in examples.items(): + url = "%s%s" % (BASE_URL, path) + + self.requests_mock.get(url, status_code=300, text=text) + versions = discover.available_versions(url) + + for v in versions: + for n in ('id', 'status', 'links'): + msg = '%s missing from %s version data' % (n, path) + self.assertThat(v, matchers.Annotate(msg, + matchers.Contains(n))) + + def test_available_versions_individual(self): + self.requests_mock.get(V3_URL, status_code=200, text=V3_VERSION_ENTRY) + + versions = discover.available_versions(V3_URL) + + for v in versions: + self.assertEqual(v['id'], 'v3.0') + self.assertEqual(v['status'], 'stable') + self.assertIn('media-types', v) + self.assertIn('links', v) + + def test_available_keystone_data(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + versions = discover.available_versions(BASE_URL) + self.assertEqual(2, len(versions)) + + for v in versions: + self.assertIn(v['id'], ('v2.0', 'v3.0')) + self.assertEqual(v['updated'], UPDATED) + self.assertEqual(v['status'], 'stable') + + if v['id'] == 'v3.0': + self.assertEqual(v['media-types'], V3_MEDIA_TYPES) + + def test_available_cinder_data(self): + text = jsonutils.dumps(CINDER_EXAMPLES) + self.requests_mock.get(BASE_URL, status_code=300, text=text) + + versions = discover.available_versions(BASE_URL) + self.assertEqual(2, len(versions)) + + for v in versions: + self.assertEqual(v['status'], 'CURRENT') + if v['id'] == 'v1.0': + self.assertEqual(v['updated'], '2012-01-04T11:33:21Z') + elif v['id'] == 'v2.0': + self.assertEqual(v['updated'], '2012-11-21T11:33:21Z') + else: + self.fail("Invalid version found") + + def test_available_glance_data(self): + text = jsonutils.dumps(GLANCE_EXAMPLES) + self.requests_mock.get(BASE_URL, status_code=200, text=text) + + versions = discover.available_versions(BASE_URL) + self.assertEqual(5, len(versions)) + + for v in versions: + if v['id'] in ('v2.2', 'v1.1'): + self.assertEqual(v['status'], 'CURRENT') + elif v['id'] in ('v2.1', 'v2.0', 'v1.0'): + self.assertEqual(v['status'], 'SUPPORTED') + else: + self.fail("Invalid version found") + + +class ClientDiscoveryTests(utils.TestCase): + + def setUp(self): + super(ClientDiscoveryTests, self).setUp() + self.deprecations.expect_deprecations() + + def assertCreatesV3(self, **kwargs): + self.requests_mock.post('%s/auth/tokens' % V3_URL, + text=V3_AUTH_RESPONSE, + headers={'X-Subject-Token': V3_TOKEN}) + + kwargs.setdefault('username', 'foo') + kwargs.setdefault('password', 'bar') + keystone = client.Client(**kwargs) + self.assertIsInstance(keystone, v3_client.Client) + return keystone + + def assertCreatesV2(self, **kwargs): + self.requests_mock.post("%s/tokens" % V2_URL, text=V2_AUTH_RESPONSE) + + kwargs.setdefault('username', 'foo') + kwargs.setdefault('password', 'bar') + keystone = client.Client(**kwargs) + self.assertIsInstance(keystone, v2_client.Client) + return keystone + + def assertVersionNotAvailable(self, **kwargs): + kwargs.setdefault('username', 'foo') + kwargs.setdefault('password', 'bar') + + self.assertRaises(exceptions.VersionNotAvailable, + client.Client, **kwargs) + + def assertDiscoveryFailure(self, **kwargs): + kwargs.setdefault('username', 'foo') + kwargs.setdefault('password', 'bar') + + self.assertRaises(exceptions.DiscoveryFailure, + client.Client, **kwargs) + + def test_discover_v3(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + self.assertCreatesV3(auth_url=BASE_URL) + + def test_discover_v2(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V2_VERSION_LIST) + self.requests_mock.post("%s/tokens" % V2_URL, text=V2_AUTH_RESPONSE) + + self.assertCreatesV2(auth_url=BASE_URL) + + def test_discover_endpoint_v2(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V2_VERSION_LIST) + self.assertCreatesV2(endpoint=BASE_URL, token='fake-token') + + def test_discover_endpoint_v3(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + self.assertCreatesV3(endpoint=BASE_URL, token='fake-token') + + def test_discover_invalid_major_version(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + self.assertVersionNotAvailable(auth_url=BASE_URL, version=5) + + def test_discover_200_response_fails(self): + self.requests_mock.get(BASE_URL, text='ok') + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_discover_minor_greater_than_available_fails(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + self.assertVersionNotAvailable(endpoint=BASE_URL, version=3.4) + + def test_discover_individual_version_v2(self): + self.requests_mock.get(V2_URL, text=V2_VERSION_ENTRY) + + self.assertCreatesV2(auth_url=V2_URL) + + def test_discover_individual_version_v3(self): + self.requests_mock.get(V3_URL, text=V3_VERSION_ENTRY) + + self.assertCreatesV3(auth_url=V3_URL) + + def test_discover_individual_endpoint_v2(self): + self.requests_mock.get(V2_URL, text=V2_VERSION_ENTRY) + self.assertCreatesV2(endpoint=V2_URL, token='fake-token') + + def test_discover_individual_endpoint_v3(self): + self.requests_mock.get(V3_URL, text=V3_VERSION_ENTRY) + self.assertCreatesV3(endpoint=V3_URL, token='fake-token') + + def test_discover_fail_to_create_bad_individual_version(self): + self.requests_mock.get(V2_URL, text=V2_VERSION_ENTRY) + self.requests_mock.get(V3_URL, text=V3_VERSION_ENTRY) + + self.assertVersionNotAvailable(auth_url=V2_URL, version=3) + self.assertVersionNotAvailable(auth_url=V3_URL, version=2) + + def test_discover_unstable_versions(self): + version_list = fixture.DiscoveryList(BASE_URL, v3_status='beta') + self.requests_mock.get(BASE_URL, status_code=300, json=version_list) + + self.assertCreatesV2(auth_url=BASE_URL) + self.assertVersionNotAvailable(auth_url=BASE_URL, version=3) + self.assertCreatesV3(auth_url=BASE_URL, unstable=True) + + def test_discover_forwards_original_ip(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + ip = '192.168.1.1' + self.assertCreatesV3(auth_url=BASE_URL, original_ip=ip) + + self.assertThat(self.requests_mock.last_request.headers['forwarded'], + matchers.Contains(ip)) + + def test_discover_bad_args(self): + self.assertRaises(exceptions.DiscoveryFailure, + client.Client) + + def test_discover_bad_response(self): + self.requests_mock.get(BASE_URL, status_code=300, json={'FOO': 'BAR'}) + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_discovery_ignore_invalid(self): + resp = [{'id': 'v3.0', + 'links': [1, 2, 3, 4], # invalid links + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED}] + self.requests_mock.get(BASE_URL, status_code=300, + text=_create_version_list(resp)) + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_ignore_entry_without_links(self): + v3 = V3_VERSION.copy() + v3['links'] = [] + self.requests_mock.get(BASE_URL, status_code=300, + text=_create_version_list([v3, V2_VERSION])) + self.assertCreatesV2(auth_url=BASE_URL) + + def test_ignore_entry_without_status(self): + v3 = V3_VERSION.copy() + del v3['status'] + self.requests_mock.get(BASE_URL, status_code=300, + text=_create_version_list([v3, V2_VERSION])) + self.assertCreatesV2(auth_url=BASE_URL) + + def test_greater_version_than_required(self): + versions = fixture.DiscoveryList(BASE_URL, v3_id='v3.6') + self.requests_mock.get(BASE_URL, json=versions) + self.assertCreatesV3(auth_url=BASE_URL, version=(3, 4)) + + def test_lesser_version_than_required(self): + versions = fixture.DiscoveryList(BASE_URL, v3_id='v3.4') + self.requests_mock.get(BASE_URL, json=versions) + self.assertVersionNotAvailable(auth_url=BASE_URL, version=(3, 6)) + + def test_bad_response(self): + self.requests_mock.get(BASE_URL, status_code=300, text="Ugly Duckling") + self.assertDiscoveryFailure(auth_url=BASE_URL) + + def test_pass_client_arguments(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V2_VERSION_LIST) + kwargs = {'original_ip': '100', 'use_keyring': False, + 'stale_duration': 15} + + cl = self.assertCreatesV2(auth_url=BASE_URL, **kwargs) + + with self.deprecations.expect_deprecations_here(): + self.assertEqual(cl.original_ip, '100') + self.assertEqual(cl.stale_duration, 15) + self.assertFalse(cl.use_keyring) + + def test_overriding_stored_kwargs(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + self.requests_mock.post("%s/auth/tokens" % V3_URL, + text=V3_AUTH_RESPONSE, + headers={'X-Subject-Token': V3_TOKEN}) + + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL, debug=False, + username='foo') + client = disc.create_client(debug=True, password='bar') + + self.assertIsInstance(client, v3_client.Client) + self.assertFalse(disc._client_kwargs['debug']) + self.assertEqual(client.username, 'foo') + self.assertEqual(client.password, 'bar') + + def test_available_versions(self): + self.requests_mock.get(BASE_URL, + status_code=300, + text=V3_VERSION_ENTRY) + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) + + with self.deprecations.expect_deprecations_here(): + versions = disc.available_versions() + self.assertEqual(1, len(versions)) + self.assertEqual(V3_VERSION, versions[0]) + + def test_unknown_client_version(self): + V4_VERSION = {'id': 'v4.0', + 'links': [{'href': 'http://url', 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED} + versions = fixture.DiscoveryList() + versions.add_version(V4_VERSION) + self.requests_mock.get(BASE_URL, status_code=300, json=versions) + + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) + self.assertRaises(exceptions.DiscoveryFailure, + disc.create_client, version=4) + + def test_discovery_fail_for_missing_v3(self): + versions = fixture.DiscoveryList(v2=True, v3=False) + self.requests_mock.get(BASE_URL, status_code=300, json=versions) + + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) + self.assertRaises(exceptions.DiscoveryFailure, + disc.create_client, version=(3, 0)) + + def _do_discovery_call(self, token=None, **kwargs): + self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + if not token: + token = uuid.uuid4().hex + + url = 'http://testurl' + + with self.deprecations.expect_deprecations_here(): + a = token_endpoint.Token(url, token) + s = session.Session(auth=a) + + # will default to true as there is a plugin on the session + discover.Discover(s, auth_url=BASE_URL, **kwargs) + + self.assertEqual(BASE_URL, self.requests_mock.last_request.url) + + def test_setting_authenticated_true(self): + token = uuid.uuid4().hex + self._do_discovery_call(token) + self.assertRequestHeaderEqual('X-Auth-Token', token) + + def test_setting_authenticated_false(self): + self._do_discovery_call(authenticated=False) + self.assertNotIn('X-Auth-Token', + self.requests_mock.last_request.headers) + + +class DiscoverQueryTests(utils.TestCase): + + def test_available_keystone_data(self): + self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) + + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) + versions = disc.version_data() + + self.assertEqual((2, 0), versions[0]['version']) + self.assertEqual('stable', versions[0]['raw_status']) + self.assertEqual(V2_URL, versions[0]['url']) + self.assertEqual((3, 0), versions[1]['version']) + self.assertEqual('stable', versions[1]['raw_status']) + self.assertEqual(V3_URL, versions[1]['url']) + + version = disc.data_for('v3.0') + self.assertEqual((3, 0), version['version']) + self.assertEqual('stable', version['raw_status']) + self.assertEqual(V3_URL, version['url']) + + version = disc.data_for(2) + self.assertEqual((2, 0), version['version']) + self.assertEqual('stable', version['raw_status']) + self.assertEqual(V2_URL, version['url']) + + self.assertIsNone(disc.url_for('v4')) + self.assertEqual(V3_URL, disc.url_for('v3')) + self.assertEqual(V2_URL, disc.url_for('v2')) + + def test_available_cinder_data(self): + text = jsonutils.dumps(CINDER_EXAMPLES) + self.requests_mock.get(BASE_URL, status_code=300, text=text) + + v1_url = "%sv1/" % BASE_URL + v2_url = "%sv2/" % BASE_URL + + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) + versions = disc.version_data() + + self.assertEqual((1, 0), versions[0]['version']) + self.assertEqual('CURRENT', versions[0]['raw_status']) + self.assertEqual(v1_url, versions[0]['url']) + self.assertEqual((2, 0), versions[1]['version']) + self.assertEqual('CURRENT', versions[1]['raw_status']) + self.assertEqual(v2_url, versions[1]['url']) + + version = disc.data_for('v2.0') + self.assertEqual((2, 0), version['version']) + self.assertEqual('CURRENT', version['raw_status']) + self.assertEqual(v2_url, version['url']) + + version = disc.data_for(1) + self.assertEqual((1, 0), version['version']) + self.assertEqual('CURRENT', version['raw_status']) + self.assertEqual(v1_url, version['url']) + + self.assertIsNone(disc.url_for('v3')) + self.assertEqual(v2_url, disc.url_for('v2')) + self.assertEqual(v1_url, disc.url_for('v1')) + + def test_available_glance_data(self): + text = jsonutils.dumps(GLANCE_EXAMPLES) + self.requests_mock.get(BASE_URL, text=text) + + v1_url = "%sv1/" % BASE_URL + v2_url = "%sv2/" % BASE_URL + + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) + versions = disc.version_data() + + self.assertEqual((1, 0), versions[0]['version']) + self.assertEqual('SUPPORTED', versions[0]['raw_status']) + self.assertEqual(v1_url, versions[0]['url']) + self.assertEqual((1, 1), versions[1]['version']) + self.assertEqual('CURRENT', versions[1]['raw_status']) + self.assertEqual(v1_url, versions[1]['url']) + self.assertEqual((2, 0), versions[2]['version']) + self.assertEqual('SUPPORTED', versions[2]['raw_status']) + self.assertEqual(v2_url, versions[2]['url']) + self.assertEqual((2, 1), versions[3]['version']) + self.assertEqual('SUPPORTED', versions[3]['raw_status']) + self.assertEqual(v2_url, versions[3]['url']) + self.assertEqual((2, 2), versions[4]['version']) + self.assertEqual('CURRENT', versions[4]['raw_status']) + self.assertEqual(v2_url, versions[4]['url']) + + for ver in (2, 2.1, 2.2): + version = disc.data_for(ver) + self.assertEqual((2, 2), version['version']) + self.assertEqual('CURRENT', version['raw_status']) + self.assertEqual(v2_url, version['url']) + self.assertEqual(v2_url, disc.url_for(ver)) + + for ver in (1, 1.1): + version = disc.data_for(ver) + self.assertEqual((1, 1), version['version']) + self.assertEqual('CURRENT', version['raw_status']) + self.assertEqual(v1_url, version['url']) + self.assertEqual(v1_url, disc.url_for(ver)) + + self.assertIsNone(disc.url_for('v3')) + self.assertIsNone(disc.url_for('v2.3')) + + def test_allow_deprecated(self): + status = 'deprecated' + version_list = [{'id': 'v3.0', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': status, + 'updated': UPDATED}] + text = jsonutils.dumps({'versions': version_list}) + self.requests_mock.get(BASE_URL, text=text) + + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) + + # deprecated is allowed by default + versions = disc.version_data(allow_deprecated=False) + self.assertEqual(0, len(versions)) + + versions = disc.version_data(allow_deprecated=True) + self.assertEqual(1, len(versions)) + self.assertEqual(status, versions[0]['raw_status']) + self.assertEqual(V3_URL, versions[0]['url']) + self.assertEqual((3, 0), versions[0]['version']) + + def test_allow_experimental(self): + status = 'experimental' + version_list = [{'id': 'v3.0', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': status, + 'updated': UPDATED}] + text = jsonutils.dumps({'versions': version_list}) + self.requests_mock.get(BASE_URL, text=text) + + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) + + versions = disc.version_data() + self.assertEqual(0, len(versions)) + + versions = disc.version_data(allow_experimental=True) + self.assertEqual(1, len(versions)) + self.assertEqual(status, versions[0]['raw_status']) + self.assertEqual(V3_URL, versions[0]['url']) + self.assertEqual((3, 0), versions[0]['version']) + + def test_allow_unknown(self): + status = 'abcdef' + version_list = fixture.DiscoveryList(BASE_URL, v2=False, + v3_status=status) + self.requests_mock.get(BASE_URL, json=version_list) + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) + + versions = disc.version_data() + self.assertEqual(0, len(versions)) + + versions = disc.version_data(allow_unknown=True) + self.assertEqual(1, len(versions)) + self.assertEqual(status, versions[0]['raw_status']) + self.assertEqual(V3_URL, versions[0]['url']) + self.assertEqual((3, 0), versions[0]['version']) + + def test_ignoring_invalid_lnks(self): + version_list = [{'id': 'v3.0', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED}, + {'id': 'v3.1', + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED}, + {'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED, + 'links': [{'href': V3_URL, 'rel': 'self'}], + }] + + text = jsonutils.dumps({'versions': version_list}) + self.requests_mock.get(BASE_URL, text=text) + + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) + + # raw_version_data will return all choices, even invalid ones + versions = disc.raw_version_data() + self.assertEqual(3, len(versions)) + + # only the version with both id and links will be actually returned + versions = disc.version_data() + self.assertEqual(1, len(versions)) + + +class CatalogHackTests(utils.TestCase): + + TEST_URL = 'http://keystone.server:5000/v2.0' + OTHER_URL = 'http://other.server:5000/path' + + IDENTITY = 'identity' + + BASE_URL = 'http://keystone.server:5000/' + V2_URL = BASE_URL + 'v2.0' + V3_URL = BASE_URL + 'v3' + + def setUp(self): + super(CatalogHackTests, self).setUp() + self.hacks = _discover._VersionHacks() + self.hacks.add_discover_hack(self.IDENTITY, + re.compile('/v2.0/?$'), + '/') + + def test_version_hacks(self): + self.assertEqual(self.BASE_URL, + self.hacks.get_discover_hack(self.IDENTITY, + self.V2_URL)) + + self.assertEqual(self.BASE_URL, + self.hacks.get_discover_hack(self.IDENTITY, + self.V2_URL + '/')) + + self.assertEqual(self.OTHER_URL, + self.hacks.get_discover_hack(self.IDENTITY, + self.OTHER_URL)) + + def test_ignored_non_service_type(self): + self.assertEqual(self.V2_URL, + self.hacks.get_discover_hack('other', self.V2_URL)) + + +class DiscoverUtils(utils.TestCase): + + def test_version_number(self): + def assertVersion(inp, out): + self.assertEqual(out, _discover.normalize_version_number(inp)) + + def versionRaises(inp): + self.assertRaises(TypeError, + _discover.normalize_version_number, + inp) + + assertVersion('v1.2', (1, 2)) + assertVersion('v11', (11, 0)) + assertVersion('1.2', (1, 2)) + assertVersion('1.5.1', (1, 5, 1)) + assertVersion('1', (1, 0)) + assertVersion(1, (1, 0)) + assertVersion(5.2, (5, 2)) + assertVersion((6, 1), (6, 1)) + assertVersion([1, 4], (1, 4)) + + versionRaises('hello') + versionRaises('1.a') + versionRaises('vacuum') diff --git a/keystoneclient/tests/unit/test_ec2utils.py b/keystoneclient/tests/unit/test_ec2utils.py new file mode 100644 index 000000000..12cf57531 --- /dev/null +++ b/keystoneclient/tests/unit/test_ec2utils.py @@ -0,0 +1,304 @@ +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import testtools + +from keystoneclient.contrib.ec2 import utils +from keystoneclient.tests.unit import client_fixtures + + +class Ec2SignerTest(testtools.TestCase): + + def setUp(self): + super(Ec2SignerTest, self).setUp() + self.useFixture(client_fixtures.Deprecations()) + self.access = '966afbde20b84200ae4e62e09acf46b2' + self.secret = '89cdf9e94e2643cab35b8b8ac5a51f83' + self.signer = utils.Ec2Signer(self.secret) + + def test_v4_creds_header(self): + auth_str = 'AWS4-HMAC-SHA256 blah' + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {}, + 'headers': {'Authorization': auth_str}} + self.assertTrue(self.signer._v4_creds(credentials)) + + def test_v4_creds_param(self): + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'X-Amz-Algorithm': 'AWS4-HMAC-SHA256'}, + 'headers': {}} + self.assertTrue(self.signer._v4_creds(credentials)) + + def test_v4_creds_false(self): + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '0', + 'AWSAccessKeyId': self.access, + 'Timestamp': '2012-11-27T11:47:02Z', + 'Action': 'Foo'}} + self.assertFalse(self.signer._v4_creds(credentials)) + + def test_generate_0(self): + """Test generate function for v0 signature.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '0', + 'AWSAccessKeyId': self.access, + 'Timestamp': '2012-11-27T11:47:02Z', + 'Action': 'Foo'}} + signature = self.signer.generate(credentials) + expected = 'SmXQEZAUdQw5glv5mX8mmixBtas=' + self.assertEqual(signature, expected) + + def test_generate_1(self): + """Test generate function for v1 signature.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '1', + 'AWSAccessKeyId': self.access}} + signature = self.signer.generate(credentials) + expected = 'VRnoQH/EhVTTLhwRLfuL7jmFW9c=' + self.assertEqual(signature, expected) + + def test_generate_v2_SHA256(self): + """Test generate function for v2 signature, SHA256.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '2', + 'AWSAccessKeyId': self.access}} + signature = self.signer.generate(credentials) + expected = 'odsGmT811GffUO0Eu13Pq+xTzKNIjJ6NhgZU74tYX/w=' + self.assertEqual(signature, expected) + + def test_generate_v2_SHA1(self): + """Test generate function for v2 signature, SHA1.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '2', + 'AWSAccessKeyId': self.access}} + self.signer.hmac_256 = None + signature = self.signer.generate(credentials) + expected = 'ZqCxMI4ZtTXWI175743mJ0hy/Gc=' + self.assertEqual(signature, expected) + + def test_generate_v4(self): + """Test v4 generator with data from AWS docs example. + + see: + http://docs.aws.amazon.com/general/latest/gr/ + sigv4-create-canonical-request.html + and + http://docs.aws.amazon.com/general/latest/gr/ + sigv4-signed-request-examples.html + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'iam.amazonaws.com', + 'Authorization': auth_str} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {'Action': 'CreateUser', + 'UserName': 'NewUser', + 'Version': '2010-05-08', + 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256', + 'X-Amz-Credential': 'AKIAEXAMPLE/20140611/' + 'us-east-1/iam/aws4_request', + 'X-Amz-Date': '20140611T231318Z', + 'X-Amz-Expires': '30', + 'X-Amz-SignedHeaders': 'host', + 'X-Amz-Signature': 'ced6826de92d2bdeed8f846f0bf508e8' + '559e98e4b0199114b84c54174deb456c'} + credentials = {'host': 'iam.amazonaws.com', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + expected = ('ced6826de92d2bdeed8f846f0bf508e8' + '559e98e4b0199114b84c54174deb456c') + self.assertEqual(signature, expected) + + def test_generate_v4_port(self): + """Test v4 generator with host:port format.""" + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('26dd92ea79aaa49f533d13b1055acdc' + 'd7d7321460d64621f96cc79c4f4d4ab2b') + self.assertEqual(signature, expected) + + def test_generate_v4_port_strip(self): + """Test v4 generator with host:port format for old boto version. + + Validate for old (<2.9.3) version of boto, where the port should + be stripped to match boto behavior. + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str, + 'User-Agent': 'Boto/2.9.2 (linux2)'} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('9a4b2276a5039ada3b90f72ea8ec1745' + '14b92b909fb106b22ad910c5d75a54f4') + self.assertEqual(expected, signature) + + def test_generate_v4_port_nostrip(self): + """Test v4 generator with host:port format for new boto version. + + Validate for new (>=2.9.3) version of boto, where the port should + not be stripped. + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str, + 'User-Agent': 'Boto/2.9.3 (linux2)'} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('26dd92ea79aaa49f533d13b1055acdc' + 'd7d7321460d64621f96cc79c4f4d4ab2b') + self.assertEqual(expected, signature) + + def test_generate_v4_port_malformed_version(self): + """Test v4 generator with host:port format for malformed boto version. + + Validate for malformed version of boto, where the port should + not be stripped. + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str, + 'User-Agent': 'Boto/2.922 (linux2)'} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('26dd92ea79aaa49f533d13b1055acdc' + 'd7d7321460d64621f96cc79c4f4d4ab2b') + self.assertEqual(expected, signature) diff --git a/keystoneclient/tests/unit/test_fixtures.py b/keystoneclient/tests/unit/test_fixtures.py new file mode 100644 index 000000000..d7fd26e66 --- /dev/null +++ b/keystoneclient/tests/unit/test_fixtures.py @@ -0,0 +1,251 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + + +from keystoneclient import fixture +from keystoneclient.tests.unit import utils + + +class V2TokenTests(utils.TestCase): + + def test_unscoped(self): + token_id = uuid.uuid4().hex + user_id = uuid.uuid4().hex + user_name = uuid.uuid4().hex + + token = fixture.V2Token(token_id=token_id, + user_id=user_id, + user_name=user_name) + + self.assertEqual(token_id, token.token_id) + self.assertEqual(token_id, token['access']['token']['id']) + self.assertEqual(user_id, token.user_id) + self.assertEqual(user_id, token['access']['user']['id']) + self.assertEqual(user_name, token.user_name) + self.assertEqual(user_name, token['access']['user']['name']) + self.assertIsNone(token.trust_id) + + def test_tenant_scoped(self): + tenant_id = uuid.uuid4().hex + tenant_name = uuid.uuid4().hex + + token = fixture.V2Token(tenant_id=tenant_id, + tenant_name=tenant_name) + + self.assertEqual(tenant_id, token.tenant_id) + self.assertEqual(tenant_id, token['access']['token']['tenant']['id']) + self.assertEqual(tenant_name, token.tenant_name) + tn = token['access']['token']['tenant']['name'] + self.assertEqual(tenant_name, tn) + self.assertIsNone(token.trust_id) + + def test_trust_scoped(self): + trust_id = uuid.uuid4().hex + trustee_user_id = uuid.uuid4().hex + + token = fixture.V2Token(trust_id=trust_id, + trustee_user_id=trustee_user_id) + trust = token['access']['trust'] + + self.assertEqual(trust_id, token.trust_id) + self.assertEqual(trust_id, trust['id']) + self.assertEqual(trustee_user_id, token.trustee_user_id) + self.assertEqual(trustee_user_id, trust['trustee_user_id']) + + def test_roles(self): + role_id1 = uuid.uuid4().hex + role_name1 = uuid.uuid4().hex + role_id2 = uuid.uuid4().hex + role_name2 = uuid.uuid4().hex + + token = fixture.V2Token() + token.add_role(id=role_id1, name=role_name1) + token.add_role(id=role_id2, name=role_name2) + + role_names = token['access']['user']['roles'] + role_ids = token['access']['metadata']['roles'] + + self.assertEqual(set([role_id1, role_id2]), set(role_ids)) + for r in (role_name1, role_name2): + self.assertIn({'name': r}, role_names) + + def test_services(self): + service_type = uuid.uuid4().hex + service_name = uuid.uuid4().hex + endpoint_id = uuid.uuid4().hex + region = uuid.uuid4().hex + + public = uuid.uuid4().hex + admin = uuid.uuid4().hex + internal = uuid.uuid4().hex + + token = fixture.V2Token() + svc = token.add_service(type=service_type, name=service_name) + + svc.add_endpoint(public=public, + admin=admin, + internal=internal, + region=region, + id=endpoint_id) + + self.assertEqual(1, len(token['access']['serviceCatalog'])) + service = token['access']['serviceCatalog'][0]['endpoints'][0] + + self.assertEqual(public, service['publicURL']) + self.assertEqual(internal, service['internalURL']) + self.assertEqual(admin, service['adminURL']) + self.assertEqual(region, service['region']) + self.assertEqual(endpoint_id, service['id']) + + +class V3TokenTests(utils.TestCase): + + def test_unscoped(self): + user_id = uuid.uuid4().hex + user_name = uuid.uuid4().hex + user_domain_id = uuid.uuid4().hex + user_domain_name = uuid.uuid4().hex + + token = fixture.V3Token(user_id=user_id, + user_name=user_name, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name) + + self.assertEqual(user_id, token.user_id) + self.assertEqual(user_id, token['token']['user']['id']) + self.assertEqual(user_name, token.user_name) + self.assertEqual(user_name, token['token']['user']['name']) + + user_domain = token['token']['user']['domain'] + + self.assertEqual(user_domain_id, token.user_domain_id) + self.assertEqual(user_domain_id, user_domain['id']) + self.assertEqual(user_domain_name, token.user_domain_name) + self.assertEqual(user_domain_name, user_domain['name']) + + def test_project_scoped(self): + project_id = uuid.uuid4().hex + project_name = uuid.uuid4().hex + project_domain_id = uuid.uuid4().hex + project_domain_name = uuid.uuid4().hex + + token = fixture.V3Token(project_id=project_id, + project_name=project_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name) + + self.assertEqual(project_id, token.project_id) + self.assertEqual(project_id, token['token']['project']['id']) + self.assertEqual(project_name, token.project_name) + self.assertEqual(project_name, token['token']['project']['name']) + + project_domain = token['token']['project']['domain'] + + self.assertEqual(project_domain_id, token.project_domain_id) + self.assertEqual(project_domain_id, project_domain['id']) + self.assertEqual(project_domain_name, token.project_domain_name) + self.assertEqual(project_domain_name, project_domain['name']) + + def test_domain_scoped(self): + domain_id = uuid.uuid4().hex + domain_name = uuid.uuid4().hex + + token = fixture.V3Token(domain_id=domain_id, + domain_name=domain_name) + + self.assertEqual(domain_id, token.domain_id) + self.assertEqual(domain_id, token['token']['domain']['id']) + self.assertEqual(domain_name, token.domain_name) + self.assertEqual(domain_name, token['token']['domain']['name']) + + def test_roles(self): + role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + + token = fixture.V3Token() + token.add_role(**role1) + token.add_role(**role2) + + self.assertEqual(2, len(token['token']['roles'])) + + self.assertIn(role1, token['token']['roles']) + self.assertIn(role2, token['token']['roles']) + + def test_trust_scoped(self): + trust_id = uuid.uuid4().hex + trustee_user_id = uuid.uuid4().hex + trustor_user_id = uuid.uuid4().hex + impersonation = True + + token = fixture.V3Token(trust_id=trust_id, + trustee_user_id=trustee_user_id, + trustor_user_id=trustor_user_id, + trust_impersonation=impersonation) + + trust = token['token']['OS-TRUST:trust'] + self.assertEqual(trust_id, token.trust_id) + self.assertEqual(trust_id, trust['id']) + self.assertEqual(trustee_user_id, token.trustee_user_id) + self.assertEqual(trustee_user_id, trust['trustee_user']['id']) + self.assertEqual(trustor_user_id, token.trustor_user_id) + self.assertEqual(trustor_user_id, trust['trustor_user']['id']) + self.assertEqual(impersonation, token.trust_impersonation) + self.assertEqual(impersonation, trust['impersonation']) + + def test_oauth_scoped(self): + access_id = uuid.uuid4().hex + consumer_id = uuid.uuid4().hex + + token = fixture.V3Token(oauth_access_token_id=access_id, + oauth_consumer_id=consumer_id) + + oauth = token['token']['OS-OAUTH1'] + + self.assertEqual(access_id, token.oauth_access_token_id) + self.assertEqual(access_id, oauth['access_token_id']) + self.assertEqual(consumer_id, token.oauth_consumer_id) + self.assertEqual(consumer_id, oauth['consumer_id']) + + def test_catalog(self): + service_type = uuid.uuid4().hex + service_name = uuid.uuid4().hex + service_id = uuid.uuid4().hex + region = uuid.uuid4().hex + endpoints = {'public': uuid.uuid4().hex, + 'internal': uuid.uuid4().hex, + 'admin': uuid.uuid4().hex} + + token = fixture.V3Token() + svc = token.add_service(type=service_type, + name=service_name, + id=service_id) + svc.add_standard_endpoints(region=region, **endpoints) + + self.assertEqual(1, len(token['token']['catalog'])) + service = token['token']['catalog'][0] + self.assertEqual(3, len(service['endpoints'])) + + self.assertEqual(service_name, service['name']) + self.assertEqual(service_type, service['type']) + self.assertEqual(service_id, service['id']) + + for endpoint in service['endpoints']: + # assert an id exists for each endpoint, remove it to make testing + # the endpoint content below easier. + self.assertTrue(endpoint.pop('id')) + + for interface, url in endpoints.items(): + endpoint = {'interface': interface, 'url': url, + 'region': region, 'region_id': region} + self.assertIn(endpoint, service['endpoints']) diff --git a/keystoneclient/tests/unit/test_http.py b/keystoneclient/tests/unit/test_http.py new file mode 100644 index 000000000..4a8053fab --- /dev/null +++ b/keystoneclient/tests/unit/test_http.py @@ -0,0 +1,205 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import io +import logging + +from testtools import matchers + +from keystoneclient import exceptions +from keystoneclient import httpclient +from keystoneclient import session +from keystoneclient.tests.unit import utils + + +RESPONSE_BODY = '{"hi": "there"}' + + +def get_client(): + cl = httpclient.HTTPClient(username="username", password="password", + project_id="tenant", auth_url="auth_test") + return cl + + +def get_authed_client(): + cl = get_client() + cl.management_url = "http://127.0.0.1:5000" + cl.auth_token = "token" + return cl + + +class ClientTest(utils.TestCase): + + TEST_URL = 'http://127.0.0.1:5000/hi' + + def test_unauthorized_client_requests(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_client() + self.assertRaises(exceptions.AuthorizationFailure, cl.get, '/hi') + self.assertRaises(exceptions.AuthorizationFailure, cl.post, '/hi') + self.assertRaises(exceptions.AuthorizationFailure, cl.put, '/hi') + self.assertRaises(exceptions.AuthorizationFailure, cl.delete, '/hi') + + def test_get(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_authed_client() + + self.stub_url('GET', text=RESPONSE_BODY) + + with self.deprecations.expect_deprecations_here(): + resp, body = cl.get("/hi") + self.assertEqual(self.requests_mock.last_request.method, 'GET') + self.assertEqual(self.requests_mock.last_request.url, self.TEST_URL) + + self.assertRequestHeaderEqual('X-Auth-Token', 'token') + self.assertRequestHeaderEqual('User-Agent', httpclient.USER_AGENT) + + # Automatic JSON parsing + self.assertEqual(body, {"hi": "there"}) + + def test_get_error_with_plaintext_resp(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_authed_client() + self.stub_url('GET', status_code=400, + text='Some evil plaintext string') + + self.assertRaises(exceptions.BadRequest, cl.get, '/hi') + + def test_get_error_with_json_resp(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_authed_client() + err_response = { + "error": { + "code": 400, + "title": "Error title", + "message": "Error message string" + } + } + self.stub_url('GET', status_code=400, json=err_response) + exc_raised = False + try: + with self.deprecations.expect_deprecations_here(): + cl.get('/hi') + except exceptions.BadRequest as exc: + exc_raised = True + self.assertEqual(exc.message, "Error message string (HTTP 400)") + self.assertTrue(exc_raised, 'Exception not raised.') + + def test_post(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_authed_client() + + self.stub_url('POST') + with self.deprecations.expect_deprecations_here(): + cl.post("/hi", body=[1, 2, 3]) + + self.assertEqual(self.requests_mock.last_request.method, 'POST') + self.assertEqual(self.requests_mock.last_request.body, '[1, 2, 3]') + + self.assertRequestHeaderEqual('X-Auth-Token', 'token') + self.assertRequestHeaderEqual('Content-Type', 'application/json') + self.assertRequestHeaderEqual('User-Agent', httpclient.USER_AGENT) + + def test_forwarded_for(self): + ORIGINAL_IP = "10.100.100.1" + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = httpclient.HTTPClient(username="username", + password="password", + project_id="tenant", + auth_url="auth_test", + original_ip=ORIGINAL_IP) + + self.stub_url('GET') + + with self.deprecations.expect_deprecations_here(): + cl.request(self.TEST_URL, 'GET') + forwarded = "for=%s;by=%s" % (ORIGINAL_IP, httpclient.USER_AGENT) + self.assertRequestHeaderEqual('Forwarded', forwarded) + + +class BasicRequestTests(utils.TestCase): + + url = 'http://keystone.test.com/' + + def setUp(self): + super(BasicRequestTests, self).setUp() + self.logger_message = io.StringIO() + handler = logging.StreamHandler(self.logger_message) + handler.setLevel(logging.DEBUG) + + self.logger = logging.getLogger(session.__name__) + level = self.logger.getEffectiveLevel() + self.logger.setLevel(logging.DEBUG) + self.logger.addHandler(handler) + + self.addCleanup(self.logger.removeHandler, handler) + self.addCleanup(self.logger.setLevel, level) + + def request(self, method='GET', response='Test Response', status_code=200, + url=None, headers={}, **kwargs): + if not url: + url = self.url + + self.requests_mock.register_uri(method, url, text=response, + status_code=status_code, + headers=headers) + + with self.deprecations.expect_deprecations_here(): + return httpclient.request(url, method, headers=headers, **kwargs) + + def test_basic_params(self): + method = 'GET' + response = 'Test Response' + status = 200 + + self.request(method=method, status_code=status, response=response, + headers={'Content-Type': 'application/json'}) + + self.assertEqual(self.requests_mock.last_request.method, method) + + logger_message = self.logger_message.getvalue() + + self.assertThat(logger_message, matchers.Contains('curl')) + self.assertThat(logger_message, matchers.Contains('-X %s' % + method)) + self.assertThat(logger_message, matchers.Contains(self.url)) + + self.assertThat(logger_message, matchers.Contains(str(status))) + self.assertThat(logger_message, matchers.Contains(response)) + + def test_headers(self): + headers = {'key': 'val', 'test': 'other'} + + self.request(headers=headers) + + for k, v in headers.items(): + self.assertRequestHeaderEqual(k, v) + + for header in headers.items(): + self.assertThat(self.logger_message.getvalue(), + matchers.Contains('-H "%s: %s"' % header)) + + def test_body(self): + data = "BODY DATA" + self.request(response=data, + headers={'Content-Type': 'application/json'}) + logger_message = self.logger_message.getvalue() + self.assertThat(logger_message, matchers.Contains('BODY:')) + self.assertThat(logger_message, matchers.Contains(data)) diff --git a/keystoneclient/tests/unit/test_https.py b/keystoneclient/tests/unit/test_https.py new file mode 100644 index 000000000..315a17aaa --- /dev/null +++ b/keystoneclient/tests/unit/test_https.py @@ -0,0 +1,106 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import requests +from unittest import mock + +from keystoneclient import httpclient +from keystoneclient.tests.unit import utils + + +FAKE_RESPONSE = utils.test_response(json={'hi': 'there'}) + +REQUEST_URL = 'https://127.0.0.1:5000/hi' +RESPONSE_BODY = '{"hi": "there"}' + + +def get_client(): + cl = httpclient.HTTPClient(username="username", password="password", + project_id="tenant", auth_url="auth_test", + cacert="ca.pem", cert=('cert.pem', "key.pem")) + return cl + + +def get_authed_client(): + cl = get_client() + cl.management_url = "https://127.0.0.1:5000" + cl.auth_token = "token" + return cl + + +class ClientTest(utils.TestCase): + + @mock.patch.object(requests, 'request') + def test_get(self, MOCK_REQUEST): + MOCK_REQUEST.return_value = FAKE_RESPONSE + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_authed_client() + + with self.deprecations.expect_deprecations_here(): + resp, body = cl.get("/hi") + + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args + + self.assertEqual(mock_args[0], 'GET') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') + + # Automatic JSON parsing + self.assertEqual(body, {"hi": "there"}) + + @mock.patch.object(requests, 'request') + def test_post(self, MOCK_REQUEST): + MOCK_REQUEST.return_value = FAKE_RESPONSE + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_authed_client() + + with self.deprecations.expect_deprecations_here(): + cl.post("/hi", body=[1, 2, 3]) + + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args + + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['data'], '[1, 2, 3]') + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') + + @mock.patch.object(requests, 'request') + def test_post_auth(self, MOCK_REQUEST): + MOCK_REQUEST.return_value = FAKE_RESPONSE + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = httpclient.HTTPClient( + username="username", password="password", project_id="tenant", + auth_url="auth_test", cacert="ca.pem", + cert=('cert.pem', 'key.pem')) + cl.management_url = "https://127.0.0.1:5000" + cl.auth_token = "token" + with self.deprecations.expect_deprecations_here(): + cl.post("/hi", body=[1, 2, 3]) + + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args + + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['data'], '[1, 2, 3]') + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') diff --git a/keystoneclient/tests/unit/test_keyring.py b/keystoneclient/tests/unit/test_keyring.py new file mode 100644 index 000000000..0a5bf7150 --- /dev/null +++ b/keystoneclient/tests/unit/test_keyring.py @@ -0,0 +1,201 @@ +# 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 +from unittest import mock + +from oslo_utils import timeutils + +from keystoneclient import access +from keystoneclient import httpclient +from keystoneclient.tests.unit import utils +from keystoneclient.tests.unit.v2_0 import client_fixtures +from keystoneclient import utils as client_utils + +try: + import keyring # noqa + import pickle # noqa +except ImportError: + keyring = None + + +PROJECT_SCOPED_TOKEN = client_fixtures.project_scoped_token() + +# These mirror values from PROJECT_SCOPED_TOKEN +USERNAME = 'exampleuser' +AUTH_URL = 'http://public.com:5000/v2.0' +TOKEN = '04c7d5ffaeef485f9dc69c06db285bdb' + +PASSWORD = 'password' +TENANT = 'tenant' +TENANT_ID = 'tenant_id' + + +class KeyringTest(utils.TestCase): + + def setUp(self): + if keyring is None: + self.skipTest( + 'optional package keyring or pickle is not installed') + + class MemoryKeyring(keyring.backend.KeyringBackend): + """A Simple testing keyring. + + This class supports stubbing an initial password to be returned by + setting password, and allows easy password and key retrieval. Also + records if a password was retrieved. + """ + + def __init__(self): + self.key = None + self.password = None + self.fetched = False + self.get_password_called = False + self.set_password_called = False + + def supported(self): + return 1 + + def get_password(self, service, username): + self.get_password_called = True + key = username + '@' + service + # make sure we don't get passwords crossed if one is enforced. + if self.key and self.key != key: + return None + if self.password: + self.fetched = True + return self.password + + def set_password(self, service, username, password): + self.set_password_called = True + self.key = username + '@' + service + self.password = password + + super(KeyringTest, self).setUp() + self.memory_keyring = MemoryKeyring() + keyring.set_keyring(self.memory_keyring) + + def test_no_keyring_key(self): + """Test case when no keyring set. + + Ensure that if we don't have use_keyring set in the client that + the keyring is never accessed. + """ + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + project_id=TENANT_ID, auth_url=AUTH_URL) + + # stub and check that a new token is received + method = 'get_raw_token_from_identity_service' + with mock.patch.object(cl, method) as meth: + meth.return_value = (True, PROJECT_SCOPED_TOKEN) + + self.assertTrue(cl.authenticate()) + + self.assertEqual(1, meth.call_count) + + # make sure that we never touched the keyring + self.assertFalse(self.memory_keyring.get_password_called) + self.assertFalse(self.memory_keyring.set_password_called) + + def test_build_keyring_key(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + project_id=TENANT_ID, auth_url=AUTH_URL) + + keyring_key = cl._build_keyring_key(auth_url=AUTH_URL, + username=USERNAME, + tenant_name=TENANT, + tenant_id=TENANT_ID, + token=TOKEN) + + self.assertEqual(keyring_key, + '%s/%s/%s/%s/%s' % + (AUTH_URL, TENANT_ID, TENANT, TOKEN, USERNAME)) + + def test_set_and_get_keyring_expired(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + project_id=TENANT_ID, auth_url=AUTH_URL, + use_keyring=True) + + # set an expired token into the keyring + auth_ref = access.AccessInfo.factory(body=PROJECT_SCOPED_TOKEN) + expired = timeutils.utcnow() - datetime.timedelta(minutes=30) + auth_ref['token']['expires'] = client_utils.isotime(expired) + self.memory_keyring.password = pickle.dumps(auth_ref) + + # stub and check that a new token is received, so not using expired + method = 'get_raw_token_from_identity_service' + with mock.patch.object(cl, method) as meth: + meth.return_value = (True, PROJECT_SCOPED_TOKEN) + + self.assertTrue(cl.authenticate()) + + self.assertEqual(1, meth.call_count) + + # check that a value was returned from the keyring + self.assertTrue(self.memory_keyring.fetched) + + # check that the new token has been loaded into the keyring + new_auth_ref = pickle.loads(self.memory_keyring.password) + self.assertEqual(new_auth_ref['token']['expires'], + PROJECT_SCOPED_TOKEN['access']['token']['expires']) + + def test_get_keyring(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + project_id=TENANT_ID, auth_url=AUTH_URL, + use_keyring=True) + + # set an token into the keyring + auth_ref = access.AccessInfo.factory(body=PROJECT_SCOPED_TOKEN) + future = timeutils.utcnow() + datetime.timedelta(minutes=30) + auth_ref['token']['expires'] = client_utils.isotime(future) + self.memory_keyring.password = pickle.dumps(auth_ref) + + # don't stub get_raw_token so will fail if authenticate happens + + self.assertTrue(cl.authenticate()) + self.assertTrue(self.memory_keyring.fetched) + + def test_set_keyring(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + project_id=TENANT_ID, auth_url=AUTH_URL, + use_keyring=True) + + # stub and check that a new token is received + method = 'get_raw_token_from_identity_service' + with mock.patch.object(cl, method) as meth: + meth.return_value = (True, PROJECT_SCOPED_TOKEN) + + self.assertTrue(cl.authenticate()) + + self.assertEqual(1, meth.call_count) + + # we checked the keyring, but we didn't find anything + self.assertTrue(self.memory_keyring.get_password_called) + self.assertFalse(self.memory_keyring.fetched) + + # check that the new token has been loaded into the keyring + self.assertTrue(self.memory_keyring.set_password_called) + new_auth_ref = pickle.loads(self.memory_keyring.password) + self.assertEqual(new_auth_ref.auth_token, TOKEN) + self.assertEqual(new_auth_ref['token'], + PROJECT_SCOPED_TOKEN['access']['token']) + self.assertEqual(new_auth_ref.username, USERNAME) diff --git a/keystoneclient/tests/unit/test_session.py b/keystoneclient/tests/unit/test_session.py new file mode 100644 index 000000000..71a8e2743 --- /dev/null +++ b/keystoneclient/tests/unit/test_session.py @@ -0,0 +1,1105 @@ +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from io import StringIO +import itertools +import logging +from unittest import mock +import uuid + +from oslo_config import cfg +from oslo_config import fixture as config +from oslo_serialization import jsonutils +import requests +from testtools import matchers + +from keystoneclient import adapter +from keystoneclient.auth import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ +from keystoneclient import session as client_session +from keystoneclient.tests.unit import utils + + +class SessionTests(utils.TestCase): + + TEST_URL = 'http://127.0.0.1:5000/' + + def setUp(self): + super(SessionTests, self).setUp() + self.deprecations.expect_deprecations() + + def test_get(self): + session = client_session.Session() + self.stub_url('GET', text='response') + resp = session.get(self.TEST_URL) + + self.assertEqual('GET', self.requests_mock.last_request.method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + + def test_post(self): + session = client_session.Session() + self.stub_url('POST', text='response') + resp = session.post(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual('POST', self.requests_mock.last_request.method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + self.assertRequestBodyIs(json={'hello': 'world'}) + + def test_head(self): + session = client_session.Session() + self.stub_url('HEAD') + resp = session.head(self.TEST_URL) + + self.assertEqual('HEAD', self.requests_mock.last_request.method) + self.assertTrue(resp.ok) + self.assertRequestBodyIs('') + + def test_put(self): + session = client_session.Session() + self.stub_url('PUT', text='response') + resp = session.put(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual('PUT', self.requests_mock.last_request.method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + self.assertRequestBodyIs(json={'hello': 'world'}) + + def test_delete(self): + session = client_session.Session() + self.stub_url('DELETE', text='response') + resp = session.delete(self.TEST_URL) + + self.assertEqual('DELETE', self.requests_mock.last_request.method) + self.assertTrue(resp.ok) + self.assertEqual(resp.text, 'response') + + def test_patch(self): + session = client_session.Session() + self.stub_url('PATCH', text='response') + resp = session.patch(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual('PATCH', self.requests_mock.last_request.method) + self.assertTrue(resp.ok) + self.assertEqual(resp.text, 'response') + self.assertRequestBodyIs(json={'hello': 'world'}) + + def test_user_agent(self): + session = client_session.Session(user_agent='test-agent') + self.stub_url('GET', text='response') + resp = session.get(self.TEST_URL) + + self.assertTrue(resp.ok) + self.assertRequestHeaderEqual('User-Agent', 'test-agent') + + resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}) + self.assertTrue(resp.ok) + self.assertRequestHeaderEqual('User-Agent', 'new-agent') + + resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}, + user_agent='overrides-agent') + self.assertTrue(resp.ok) + self.assertRequestHeaderEqual('User-Agent', 'overrides-agent') + + def test_http_session_opts(self): + session = client_session.Session(cert='cert.pem', timeout=5, + verify='certs') + + FAKE_RESP = utils.test_response(text='resp') + RESP = mock.Mock(return_value=FAKE_RESP) + + with mock.patch.object(session.session, 'request', RESP) as mocked: + session.post(self.TEST_URL, data='value') + + mock_args, mock_kwargs = mocked.call_args + + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], self.TEST_URL) + self.assertEqual(mock_kwargs['data'], 'value') + self.assertEqual(mock_kwargs['cert'], 'cert.pem') + self.assertEqual(mock_kwargs['verify'], 'certs') + self.assertEqual(mock_kwargs['timeout'], 5) + + def test_not_found(self): + session = client_session.Session() + self.stub_url('GET', status_code=404) + self.assertRaises(exceptions.NotFound, session.get, self.TEST_URL) + + def test_server_error(self): + session = client_session.Session() + self.stub_url('GET', status_code=500) + self.assertRaises(exceptions.InternalServerError, + session.get, self.TEST_URL) + + def test_session_debug_output(self): + """Test request and response headers in debug logs. + + in order to redact secure headers while debug is true. + """ + session = client_session.Session(verify=False) + headers = {'HEADERA': 'HEADERVALB', + 'Content-Type': 'application/json'} + security_headers = {'Authorization': uuid.uuid4().hex, + 'X-Auth-Token': uuid.uuid4().hex, + 'X-Subject-Token': uuid.uuid4().hex, + 'X-Service-Token': uuid.uuid4().hex} + body = '{"a": "b"}' + data = '{"c": "d"}' + all_headers = dict( + itertools.chain(headers.items(), security_headers.items())) + self.stub_url('POST', text=body, headers=all_headers) + resp = session.post(self.TEST_URL, headers=all_headers, data=data) + self.assertEqual(resp.status_code, 200) + + self.assertIn('curl', self.logger.output) + self.assertIn('POST', self.logger.output) + self.assertIn('--insecure', self.logger.output) + self.assertIn(body, self.logger.output) + self.assertIn("'%s'" % data, self.logger.output) + + for k, v in headers.items(): + self.assertIn(k, self.logger.output) + self.assertIn(v, self.logger.output) + + # Assert that response headers contains actual values and + # only debug logs has been masked + for k, v in security_headers.items(): + self.assertIn('%s: {SHA1}' % k, self.logger.output) + self.assertEqual(v, resp.headers[k]) + self.assertNotIn(v, self.logger.output) + + def test_logs_failed_output(self): + """Test that output is logged even for failed requests.""" + session = client_session.Session() + body = {uuid.uuid4().hex: uuid.uuid4().hex} + + self.stub_url('GET', json=body, status_code=400, + headers={'Content-Type': 'application/json'}) + resp = session.get(self.TEST_URL, raise_exc=False) + + self.assertEqual(resp.status_code, 400) + self.assertIn(list(body.keys())[0], self.logger.output) + self.assertIn(list(body.values())[0], self.logger.output) + + def test_logging_body_only_for_specified_content_types(self): + """Verify response body is only logged in specific content types. + + Response bodies are logged only when the response's Content-Type header + is set to application/json. This prevents us to get an unexpected + MemoryError when reading arbitrary responses, such as streams. + """ + OMITTED_BODY = ('Omitted, Content-Type is set to %s. Only ' + 'application/json responses have their bodies logged.') + session = client_session.Session(verify=False) + + # Content-Type is not set + body = jsonutils.dumps({'token': {'id': '...'}}) + self.stub_url('POST', text=body) + session.post(self.TEST_URL) + self.assertNotIn(body, self.logger.output) + self.assertIn(OMITTED_BODY % None, self.logger.output) + + # Content-Type is set to text/xml + body = '...' + self.stub_url('POST', text=body, headers={'Content-Type': 'text/xml'}) + session.post(self.TEST_URL) + self.assertNotIn(body, self.logger.output) + self.assertIn(OMITTED_BODY % 'text/xml', self.logger.output) + + # Content-Type is set to application/json + body = jsonutils.dumps({'token': {'id': '...'}}) + self.stub_url('POST', text=body, + headers={'Content-Type': 'application/json'}) + session.post(self.TEST_URL) + self.assertIn(body, self.logger.output) + self.assertNotIn(OMITTED_BODY % 'application/json', self.logger.output) + + # Content-Type is set to application/json; charset=UTF-8 + body = jsonutils.dumps({'token': {'id': '...'}}) + self.stub_url( + 'POST', text=body, + headers={'Content-Type': 'application/json; charset=UTF-8'}) + session.post(self.TEST_URL) + self.assertIn(body, self.logger.output) + self.assertNotIn(OMITTED_BODY % 'application/json; charset=UTF-8', + self.logger.output) + + def test_unicode_data_in_debug_output(self): + """Verify that ascii-encodable data is logged without modification.""" + session = client_session.Session(verify=False) + + body = 'RESP' + data = 'αβγδ' + self.stub_url('POST', text=body) + session.post(self.TEST_URL, data=data) + + self.assertIn("'%s'" % data, self.logger.output) + + def test_logging_cacerts(self): + path_to_certs = '/path/to/certs' + session = client_session.Session(verify=path_to_certs) + + self.stub_url('GET', text='text') + session.get(self.TEST_URL) + + self.assertIn('--cacert', self.logger.output) + self.assertIn(path_to_certs, self.logger.output) + + def test_connect_retries(self): + + def _timeout_error(request, context): + raise requests.exceptions.Timeout() + + self.stub_url('GET', text=_timeout_error) + + session = client_session.Session() + retries = 3 + + with mock.patch('time.sleep') as m: + self.assertRaises(exceptions.RequestTimeout, + session.get, + self.TEST_URL, connect_retries=retries) + + self.assertEqual(retries, m.call_count) + # 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0 + m.assert_called_with(2.0) + + # we count retries so there will be one initial request + 3 retries + self.assertThat(self.requests_mock.request_history, + matchers.HasLength(retries + 1)) + + def test_uses_tcp_keepalive_by_default(self): + session = client_session.Session() + requests_session = session.session + self.assertIsInstance(requests_session.adapters['http://'], + client_session.TCPKeepAliveAdapter) + self.assertIsInstance(requests_session.adapters['https://'], + client_session.TCPKeepAliveAdapter) + + def test_does_not_set_tcp_keepalive_on_custom_sessions(self): + mock_session = mock.Mock() + client_session.Session(session=mock_session) + self.assertFalse(mock_session.mount.called) + + def test_ssl_error_message(self): + error = uuid.uuid4().hex + + def _ssl_error(request, context): + raise requests.exceptions.SSLError(error) + + self.stub_url('GET', text=_ssl_error) + session = client_session.Session() + + # The exception should contain the URL and details about the SSL error + msg = _('SSL exception connecting to %(url)s: %(error)s') % { + 'url': self.TEST_URL, 'error': error} + self.assertRaisesRegex( + exceptions.SSLError, + msg, + session.get, + self.TEST_URL, + ) + + def test_mask_password_in_http_log_response(self): + session = client_session.Session() + + def fake_debug(msg): + self.assertNotIn('verybadpass', msg) + + logger = mock.Mock(isEnabledFor=mock.Mock(return_value=True)) + logger.debug = mock.Mock(side_effect=fake_debug) + body = { + "connection_info": { + "driver_volume_type": "iscsi", + "data": { + "auth_password": "verybadpass", + "target_discovered": False, + "encrypted": False, + "qos_specs": None, + "target_iqn": ("iqn.2010-10.org.openstack:volume-" + "744d2085-8e78-40a5-8659-ef3cffb2480e"), + "target_portal": "172.99.69.228:3260", + "volume_id": "744d2085-8e78-40a5-8659-ef3cffb2480e", + "target_lun": 1, + "access_mode": "rw", + "auth_username": "verybadusername", + "auth_method": "CHAP"}}} + body_json = jsonutils.dumps(body) + response = mock.Mock(text=body_json, status_code=200, + headers={'content-type': 'application/json'}) + session._http_log_response(response, logger) + self.assertEqual(1, logger.debug.call_count) + + +class TCPKeepAliveAdapter(utils.TestCase): + + @mock.patch.object(client_session, 'socket') + @mock.patch('requests.adapters.HTTPAdapter.init_poolmanager') + def test_init_poolmanager_all_options(self, mock_parent_init_poolmanager, + mock_socket): + # properties expected to be in socket. + mock_socket.TCP_KEEPIDLE = mock.sentinel.TCP_KEEPIDLE + mock_socket.TCP_KEEPCNT = mock.sentinel.TCP_KEEPCNT + mock_socket.TCP_KEEPINTVL = mock.sentinel.TCP_KEEPINTVL + desired_opts = [mock_socket.TCP_KEEPIDLE, mock_socket.TCP_KEEPCNT, + mock_socket.TCP_KEEPINTVL] + + adapter = client_session.TCPKeepAliveAdapter() + adapter.init_poolmanager() + + call_args, call_kwargs = mock_parent_init_poolmanager.call_args + called_socket_opts = call_kwargs['socket_options'] + call_options = [opt for (protocol, opt, value) in called_socket_opts] + for opt in desired_opts: + self.assertIn(opt, call_options) + + @mock.patch.object(client_session, 'socket') + @mock.patch('requests.adapters.HTTPAdapter.init_poolmanager') + def test_init_poolmanager(self, mock_parent_init_poolmanager, mock_socket): + spec = ['IPPROTO_TCP', 'TCP_NODELAY', 'SOL_SOCKET', 'SO_KEEPALIVE'] + mock_socket.mock_add_spec(spec) + adapter = client_session.TCPKeepAliveAdapter() + adapter.init_poolmanager() + + call_args, call_kwargs = mock_parent_init_poolmanager.call_args + called_socket_opts = call_kwargs['socket_options'] + call_options = [opt for (protocol, opt, value) in called_socket_opts] + self.assertEqual([mock_socket.TCP_NODELAY, mock_socket.SO_KEEPALIVE], + call_options) + + +class RedirectTests(utils.TestCase): + + REDIRECT_CHAIN = ['http://myhost:3445/', + 'http://anotherhost:6555/', + 'http://thirdhost/', + 'http://finaldestination:55/'] + + DEFAULT_REDIRECT_BODY = 'Redirect' + DEFAULT_RESP_BODY = 'Found' + + def setUp(self): + super(RedirectTests, self).setUp() + self.deprecations.expect_deprecations() + + def setup_redirects(self, method='GET', status_code=305, + redirect_kwargs=None, final_kwargs=None): + redirect_kwargs = redirect_kwargs or {} + final_kwargs = final_kwargs or {} + redirect_kwargs.setdefault('text', self.DEFAULT_REDIRECT_BODY) + + for s, d in zip(self.REDIRECT_CHAIN, self.REDIRECT_CHAIN[1:]): + self.requests_mock.register_uri(method, s, status_code=status_code, + headers={'Location': d}, + **redirect_kwargs) + + final_kwargs.setdefault('status_code', 200) + final_kwargs.setdefault('text', self.DEFAULT_RESP_BODY) + self.requests_mock.register_uri(method, self.REDIRECT_CHAIN[-1], + **final_kwargs) + + def assertResponse(self, resp): + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.text, self.DEFAULT_RESP_BODY) + + def test_basic_get(self): + session = client_session.Session() + self.setup_redirects() + resp = session.get(self.REDIRECT_CHAIN[-2]) + self.assertResponse(resp) + + def test_basic_post_keeps_correct_method(self): + session = client_session.Session() + self.setup_redirects(method='POST', status_code=301) + resp = session.post(self.REDIRECT_CHAIN[-2]) + self.assertResponse(resp) + + def test_redirect_forever(self): + session = client_session.Session(redirect=True) + self.setup_redirects() + resp = session.get(self.REDIRECT_CHAIN[0]) + self.assertResponse(resp) + self.assertTrue(len(resp.history), len(self.REDIRECT_CHAIN)) + + def test_no_redirect(self): + session = client_session.Session(redirect=False) + self.setup_redirects() + resp = session.get(self.REDIRECT_CHAIN[0]) + self.assertEqual(resp.status_code, 305) + self.assertEqual(resp.url, self.REDIRECT_CHAIN[0]) + + def test_redirect_limit(self): + self.setup_redirects() + for i in (1, 2): + session = client_session.Session(redirect=i) + resp = session.get(self.REDIRECT_CHAIN[0]) + self.assertEqual(resp.status_code, 305) + self.assertEqual(resp.url, self.REDIRECT_CHAIN[i]) + self.assertEqual(resp.text, self.DEFAULT_REDIRECT_BODY) + + def test_history_matches_requests(self): + self.setup_redirects(status_code=301) + session = client_session.Session(redirect=True) + req_resp = requests.get(self.REDIRECT_CHAIN[0], + allow_redirects=True) + + ses_resp = session.get(self.REDIRECT_CHAIN[0]) + + self.assertEqual(len(req_resp.history), len(ses_resp.history)) + + for r, s in zip(req_resp.history, ses_resp.history): + self.assertEqual(r.url, s.url) + self.assertEqual(r.status_code, s.status_code) + + +class ConstructSessionFromArgsTests(utils.TestCase): + + KEY = 'keyfile' + CERT = 'certfile' + CACERT = 'cacert-path' + + def _s(self, k=None, **kwargs): + k = k or kwargs + with self.deprecations.expect_deprecations_here(): + return client_session.Session.construct(k) + + def test_verify(self): + self.assertFalse(self._s(insecure=True).verify) + self.assertTrue(self._s(verify=True, insecure=True).verify) + self.assertFalse(self._s(verify=False, insecure=True).verify) + self.assertEqual(self._s(cacert=self.CACERT).verify, self.CACERT) + + def test_cert(self): + tup = (self.CERT, self.KEY) + self.assertEqual(self._s(cert=tup).cert, tup) + self.assertEqual(self._s(cert=self.CERT, key=self.KEY).cert, tup) + self.assertIsNone(self._s(key=self.KEY).cert) + + def test_pass_through(self): + value = 42 # only a number because timeout needs to be + for key in ['timeout', 'session', 'original_ip', 'user_agent']: + args = {key: value} + self.assertEqual(getattr(self._s(args), key), value) + self.assertNotIn(key, args) + + +class AuthPlugin(base.BaseAuthPlugin): + """Very simple debug authentication plugin. + + Takes Parameters such that it can throw exceptions at the right times. + """ + + TEST_TOKEN = utils.TestCase.TEST_TOKEN + TEST_USER_ID = 'aUser' + TEST_PROJECT_ID = 'aProject' + + SERVICE_URLS = { + 'identity': {'public': 'http://identity-public:1111/v2.0', + 'admin': 'http://identity-admin:1111/v2.0'}, + 'compute': {'public': 'http://compute-public:2222/v1.0', + 'admin': 'http://compute-admin:2222/v1.0'}, + 'image': {'public': 'http://image-public:3333/v2.0', + 'admin': 'http://image-admin:3333/v2.0'} + } + + def __init__(self, token=TEST_TOKEN, invalidate=True): + self.token = token + self._invalidate = invalidate + + def get_token(self, session): + return self.token + + def get_endpoint(self, session, service_type=None, interface=None, + **kwargs): + try: + return self.SERVICE_URLS[service_type][interface] + except (KeyError, AttributeError): + return None + + def invalidate(self): + return self._invalidate + + def get_user_id(self, session): + return self.TEST_USER_ID + + def get_project_id(self, session): + return self.TEST_PROJECT_ID + + +class CalledAuthPlugin(base.BaseAuthPlugin): + + ENDPOINT = 'http://fakeendpoint/' + + def __init__(self, invalidate=True): + self.get_token_called = False + self.get_endpoint_called = False + self.endpoint_arguments = {} + self.invalidate_called = False + self._invalidate = invalidate + + def get_token(self, session): + self.get_token_called = True + return utils.TestCase.TEST_TOKEN + + def get_endpoint(self, session, **kwargs): + self.get_endpoint_called = True + self.endpoint_arguments = kwargs + return self.ENDPOINT + + def invalidate(self): + self.invalidate_called = True + return self._invalidate + + +class SessionAuthTests(utils.TestCase): + + TEST_URL = 'http://127.0.0.1:5000/' + TEST_JSON = {'hello': 'world'} + + def setUp(self): + super(SessionAuthTests, self).setUp() + self.deprecations.expect_deprecations() + + def stub_service_url(self, service_type, interface, path, + method='GET', **kwargs): + base_url = AuthPlugin.SERVICE_URLS[service_type][interface] + uri = "%s/%s" % (base_url.rstrip('/'), path.lstrip('/')) + + self.requests_mock.register_uri(method, uri, **kwargs) + + def test_auth_plugin_default_with_plugin(self): + self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON) + + # if there is an auth_plugin then it should default to authenticated + auth = AuthPlugin() + sess = client_session.Session(auth=auth) + resp = sess.get(self.TEST_URL) + self.assertEqual(resp.json(), self.TEST_JSON) + + self.assertRequestHeaderEqual('X-Auth-Token', AuthPlugin.TEST_TOKEN) + + def test_auth_plugin_disable(self): + self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON) + + auth = AuthPlugin() + sess = client_session.Session(auth=auth) + resp = sess.get(self.TEST_URL, authenticated=False) + self.assertEqual(resp.json(), self.TEST_JSON) + + self.assertRequestHeaderEqual('X-Auth-Token', None) + + def test_service_type_urls(self): + service_type = 'compute' + interface = 'public' + path = '/instances' + status = 200 + body = 'SUCCESS' + + self.stub_service_url(service_type=service_type, + interface=interface, + path=path, + status_code=status, + text=body) + + sess = client_session.Session(auth=AuthPlugin()) + resp = sess.get(path, + endpoint_filter={'service_type': service_type, + 'interface': interface}) + + self.assertEqual(self.requests_mock.last_request.url, + AuthPlugin.SERVICE_URLS['compute']['public'] + path) + self.assertEqual(resp.text, body) + self.assertEqual(resp.status_code, status) + + def test_service_url_raises_if_no_auth_plugin(self): + sess = client_session.Session() + self.assertRaises(exceptions.MissingAuthPlugin, + sess.get, '/path', + endpoint_filter={'service_type': 'compute', + 'interface': 'public'}) + + def test_service_url_raises_if_no_url_returned(self): + sess = client_session.Session(auth=AuthPlugin()) + self.assertRaises(exceptions.EndpointNotFound, + sess.get, '/path', + endpoint_filter={'service_type': 'unknown', + 'interface': 'public'}) + + def test_raises_exc_only_when_asked(self): + # A request that returns a HTTP error should by default raise an + # exception by default, if you specify raise_exc=False then it will not + self.requests_mock.get(self.TEST_URL, status_code=401) + + sess = client_session.Session() + self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL) + + resp = sess.get(self.TEST_URL, raise_exc=False) + self.assertEqual(401, resp.status_code) + + def test_passed_auth_plugin(self): + passed = CalledAuthPlugin() + sess = client_session.Session() + + self.requests_mock.get(CalledAuthPlugin.ENDPOINT + 'path', + status_code=200) + endpoint_filter = {'service_type': 'identity'} + + # no plugin with authenticated won't work + self.assertRaises(exceptions.MissingAuthPlugin, sess.get, 'path', + authenticated=True) + + # no plugin with an endpoint filter won't work + self.assertRaises(exceptions.MissingAuthPlugin, sess.get, 'path', + authenticated=False, endpoint_filter=endpoint_filter) + + resp = sess.get('path', auth=passed, endpoint_filter=endpoint_filter) + + self.assertEqual(200, resp.status_code) + self.assertTrue(passed.get_endpoint_called) + self.assertTrue(passed.get_token_called) + + def test_passed_auth_plugin_overrides(self): + fixed = CalledAuthPlugin() + passed = CalledAuthPlugin() + + sess = client_session.Session(fixed) + + self.requests_mock.get(CalledAuthPlugin.ENDPOINT + 'path', + status_code=200) + + resp = sess.get('path', auth=passed, + endpoint_filter={'service_type': 'identity'}) + + self.assertEqual(200, resp.status_code) + self.assertTrue(passed.get_endpoint_called) + self.assertTrue(passed.get_token_called) + self.assertFalse(fixed.get_endpoint_called) + self.assertFalse(fixed.get_token_called) + + def test_requests_auth_plugin(self): + sess = client_session.Session() + + requests_auth = object() + + FAKE_RESP = utils.test_response(text='resp') + RESP = mock.Mock(return_value=FAKE_RESP) + + with mock.patch.object(sess.session, 'request', RESP) as mocked: + sess.get(self.TEST_URL, requests_auth=requests_auth) + + mocked.assert_called_once_with('GET', self.TEST_URL, + headers=mock.ANY, + allow_redirects=mock.ANY, + auth=requests_auth, + verify=mock.ANY) + + def test_reauth_called(self): + auth = CalledAuthPlugin(invalidate=True) + sess = client_session.Session(auth=auth) + + self.requests_mock.get(self.TEST_URL, + [{'text': 'Failed', 'status_code': 401}, + {'text': 'Hello', 'status_code': 200}]) + + # allow_reauth=True is the default + resp = sess.get(self.TEST_URL, authenticated=True) + + self.assertEqual(200, resp.status_code) + self.assertEqual('Hello', resp.text) + self.assertTrue(auth.invalidate_called) + + def test_reauth_not_called(self): + auth = CalledAuthPlugin(invalidate=True) + sess = client_session.Session(auth=auth) + + self.requests_mock.get(self.TEST_URL, + [{'text': 'Failed', 'status_code': 401}, + {'text': 'Hello', 'status_code': 200}]) + + self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL, + authenticated=True, allow_reauth=False) + self.assertFalse(auth.invalidate_called) + + def test_endpoint_override_overrides_filter(self): + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + + override_base = 'http://mytest/' + path = 'path' + override_url = override_base + path + resp_text = uuid.uuid4().hex + + self.requests_mock.get(override_url, text=resp_text) + + resp = sess.get(path, + endpoint_override=override_base, + endpoint_filter={'service_type': 'identity'}) + + self.assertEqual(resp_text, resp.text) + self.assertEqual(override_url, self.requests_mock.last_request.url) + + self.assertTrue(auth.get_token_called) + self.assertFalse(auth.get_endpoint_called) + + def test_endpoint_override_ignore_full_url(self): + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + + path = 'path' + url = self.TEST_URL + path + + resp_text = uuid.uuid4().hex + self.requests_mock.get(url, text=resp_text) + + resp = sess.get(url, + endpoint_override='http://someother.url', + endpoint_filter={'service_type': 'identity'}) + + self.assertEqual(resp_text, resp.text) + self.assertEqual(url, self.requests_mock.last_request.url) + + self.assertTrue(auth.get_token_called) + self.assertFalse(auth.get_endpoint_called) + + def test_user_and_project_id(self): + auth = AuthPlugin() + sess = client_session.Session(auth=auth) + + self.assertEqual(auth.TEST_USER_ID, sess.get_user_id()) + self.assertEqual(auth.TEST_PROJECT_ID, sess.get_project_id()) + + def test_logger_object_passed(self): + logger = logging.getLogger(uuid.uuid4().hex) + logger.setLevel(logging.DEBUG) + logger.propagate = False + + io = StringIO() + handler = logging.StreamHandler(io) + logger.addHandler(handler) + + auth = AuthPlugin() + sess = client_session.Session(auth=auth) + response = {uuid.uuid4().hex: uuid.uuid4().hex} + + self.stub_url('GET', + json=response, + headers={'Content-Type': 'application/json'}) + + resp = sess.get(self.TEST_URL, logger=logger) + + self.assertEqual(response, resp.json()) + output = io.getvalue() + + self.assertIn(self.TEST_URL, output) + self.assertIn(list(response.keys())[0], output) + self.assertIn(list(response.values())[0], output) + + self.assertNotIn(list(response.keys())[0], self.logger.output) + self.assertNotIn(list(response.values())[0], self.logger.output) + + +class AdapterTest(utils.TestCase): + + SERVICE_TYPE = uuid.uuid4().hex + SERVICE_NAME = uuid.uuid4().hex + INTERFACE = uuid.uuid4().hex + REGION_NAME = uuid.uuid4().hex + USER_AGENT = uuid.uuid4().hex + VERSION = uuid.uuid4().hex + + TEST_URL = CalledAuthPlugin.ENDPOINT + + def setUp(self): + super(AdapterTest, self).setUp() + self.deprecations.expect_deprecations() + + def _create_loaded_adapter(self): + auth = CalledAuthPlugin() + sess = client_session.Session() + return adapter.Adapter(sess, + auth=auth, + service_type=self.SERVICE_TYPE, + service_name=self.SERVICE_NAME, + interface=self.INTERFACE, + region_name=self.REGION_NAME, + user_agent=self.USER_AGENT, + version=self.VERSION) + + def _verify_endpoint_called(self, adpt): + self.assertEqual(self.SERVICE_TYPE, + adpt.auth.endpoint_arguments['service_type']) + self.assertEqual(self.SERVICE_NAME, + adpt.auth.endpoint_arguments['service_name']) + self.assertEqual(self.INTERFACE, + adpt.auth.endpoint_arguments['interface']) + self.assertEqual(self.REGION_NAME, + adpt.auth.endpoint_arguments['region_name']) + self.assertEqual(self.VERSION, + adpt.auth.endpoint_arguments['version']) + + def test_setting_variables_on_request(self): + response = uuid.uuid4().hex + self.stub_url('GET', text=response) + adpt = self._create_loaded_adapter() + resp = adpt.get('/') + self.assertEqual(resp.text, response) + + self._verify_endpoint_called(adpt) + self.assertTrue(adpt.auth.get_token_called) + self.assertRequestHeaderEqual('User-Agent', self.USER_AGENT) + + def test_setting_variables_on_get_endpoint(self): + adpt = self._create_loaded_adapter() + url = adpt.get_endpoint() + + self.assertEqual(self.TEST_URL, url) + self._verify_endpoint_called(adpt) + + def test_legacy_binding(self): + key = uuid.uuid4().hex + val = uuid.uuid4().hex + response = jsonutils.dumps({key: val}) + + self.stub_url('GET', text=response) + + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + adpt = adapter.LegacyJsonAdapter(sess, + service_type=self.SERVICE_TYPE, + user_agent=self.USER_AGENT) + + resp, body = adpt.get('/') + self.assertEqual(self.SERVICE_TYPE, + auth.endpoint_arguments['service_type']) + self.assertEqual(resp.text, response) + self.assertEqual(val, body[key]) + + def test_legacy_binding_non_json_resp(self): + response = uuid.uuid4().hex + self.stub_url('GET', text=response, + headers={'Content-Type': 'text/html'}) + + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + adpt = adapter.LegacyJsonAdapter(sess, + service_type=self.SERVICE_TYPE, + user_agent=self.USER_AGENT) + + resp, body = adpt.get('/') + self.assertEqual(self.SERVICE_TYPE, + auth.endpoint_arguments['service_type']) + self.assertEqual(resp.text, response) + self.assertIsNone(body) + + def test_methods(self): + sess = client_session.Session() + adpt = adapter.Adapter(sess) + url = 'http://url' + + for method in ['get', 'head', 'post', 'put', 'patch', 'delete']: + with mock.patch.object(adpt, 'request') as m: + getattr(adpt, method)(url) + m.assert_called_once_with(url, method.upper()) + + def test_setting_endpoint_override(self): + endpoint_override = 'http://overrideurl' + path = '/path' + endpoint_url = endpoint_override + path + + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + adpt = adapter.Adapter(sess, endpoint_override=endpoint_override) + + response = uuid.uuid4().hex + self.requests_mock.get(endpoint_url, text=response) + + resp = adpt.get(path) + + self.assertEqual(response, resp.text) + self.assertEqual(endpoint_url, self.requests_mock.last_request.url) + + self.assertEqual(endpoint_override, adpt.get_endpoint()) + + def test_adapter_invalidate(self): + auth = CalledAuthPlugin() + sess = client_session.Session() + adpt = adapter.Adapter(sess, auth=auth) + + adpt.invalidate() + + self.assertTrue(auth.invalidate_called) + + def test_adapter_get_token(self): + auth = CalledAuthPlugin() + sess = client_session.Session() + adpt = adapter.Adapter(sess, auth=auth) + + self.assertEqual(self.TEST_TOKEN, adpt.get_token()) + self.assertTrue(auth.get_token_called) + + def test_adapter_connect_retries(self): + retries = 2 + sess = client_session.Session() + adpt = adapter.Adapter(sess, connect_retries=retries) + + def _refused_error(request, context): + raise requests.exceptions.ConnectionError() + + self.stub_url('GET', text=_refused_error) + + with mock.patch('time.sleep') as m: + self.assertRaises(exceptions.ConnectionRefused, + adpt.get, self.TEST_URL) + self.assertEqual(retries, m.call_count) + + # we count retries so there will be one initial request + 2 retries + self.assertThat(self.requests_mock.request_history, + matchers.HasLength(retries + 1)) + + def test_user_and_project_id(self): + auth = AuthPlugin() + sess = client_session.Session() + adpt = adapter.Adapter(sess, auth=auth) + + self.assertEqual(auth.TEST_USER_ID, adpt.get_user_id()) + self.assertEqual(auth.TEST_PROJECT_ID, adpt.get_project_id()) + + def test_logger_object_passed(self): + logger = logging.getLogger(uuid.uuid4().hex) + logger.setLevel(logging.DEBUG) + logger.propagate = False + + io = StringIO() + handler = logging.StreamHandler(io) + logger.addHandler(handler) + + auth = AuthPlugin() + sess = client_session.Session(auth=auth) + adpt = adapter.Adapter(sess, auth=auth, logger=logger) + + response = {uuid.uuid4().hex: uuid.uuid4().hex} + + self.stub_url('GET', json=response, + headers={'Content-Type': 'application/json'}) + + resp = adpt.get(self.TEST_URL, logger=logger) + + self.assertEqual(response, resp.json()) + output = io.getvalue() + + self.assertIn(self.TEST_URL, output) + self.assertIn(list(response.keys())[0], output) + self.assertIn(list(response.values())[0], output) + + self.assertNotIn(list(response.keys())[0], self.logger.output) + self.assertNotIn(list(response.values())[0], self.logger.output) + + +class ConfLoadingTests(utils.TestCase): + + GROUP = 'sessiongroup' + + def setUp(self): + super(ConfLoadingTests, self).setUp() + + self.conf_fixture = self.useFixture(config.Config()) + client_session.Session.register_conf_options(self.conf_fixture.conf, + self.GROUP) + + def config(self, **kwargs): + kwargs['group'] = self.GROUP + self.conf_fixture.config(**kwargs) + + def get_session(self, **kwargs): + with self.deprecations.expect_deprecations_here(): + return client_session.Session.load_from_conf_options( + self.conf_fixture.conf, + self.GROUP, + **kwargs) + + def test_insecure_timeout(self): + self.config(insecure=True, timeout=5) + s = self.get_session() + + self.assertFalse(s.verify) + self.assertEqual(5, s.timeout) + + def test_client_certs(self): + cert = '/path/to/certfile' + key = '/path/to/keyfile' + + self.config(certfile=cert, keyfile=key) + s = self.get_session() + + self.assertTrue(s.verify) + self.assertEqual((cert, key), s.cert) + + def test_cacert(self): + cafile = '/path/to/cacert' + + self.config(cafile=cafile) + s = self.get_session() + + self.assertEqual(cafile, s.verify) + + def test_deprecated(self): + def new_deprecated(): + return cfg.DeprecatedOpt(uuid.uuid4().hex, group=uuid.uuid4().hex) + + opt_names = ['cafile', 'certfile', 'keyfile', 'insecure', 'timeout'] + depr = dict([(n, [new_deprecated()]) for n in opt_names]) + opts = client_session.Session.get_conf_options(deprecated_opts=depr) + + self.assertThat(opt_names, matchers.HasLength(len(opts))) + for opt in opts: + self.assertIn(depr[opt.name][0], opt.deprecated_opts) + + +class CliLoadingTests(utils.TestCase): + + def setUp(self): + super(CliLoadingTests, self).setUp() + + self.parser = argparse.ArgumentParser() + client_session.Session.register_cli_options(self.parser) + + def get_session(self, val, **kwargs): + args = self.parser.parse_args(val.split()) + with self.deprecations.expect_deprecations_here(): + return client_session.Session.load_from_cli_options(args, **kwargs) + + def test_insecure_timeout(self): + s = self.get_session('--insecure --timeout 5.5') + + self.assertFalse(s.verify) + self.assertEqual(5.5, s.timeout) + + def test_client_certs(self): + cert = '/path/to/certfile' + key = '/path/to/keyfile' + + s = self.get_session('--os-cert %s --os-key %s' % (cert, key)) + + self.assertTrue(s.verify) + self.assertEqual((cert, key), s.cert) + + def test_cacert(self): + cacert = '/path/to/cacert' + + s = self.get_session('--os-cacert %s' % cacert) + + self.assertEqual(cacert, s.verify) diff --git a/keystoneclient/tests/unit/test_utils.py b/keystoneclient/tests/unit/test_utils.py new file mode 100644 index 000000000..2aa5e92be --- /dev/null +++ b/keystoneclient/tests/unit/test_utils.py @@ -0,0 +1,131 @@ +# 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 as ksa_exceptions +import testresources +from testtools import matchers + + +from keystoneclient import exceptions as ksc_exceptions +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit import utils as test_utils +from keystoneclient import utils + + +class FakeResource(object): + pass + + +class FakeManager(object): + + resource_class = FakeResource + + resources = { + '1234': {'name': 'entity_one'}, + '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0': {'name': 'entity_two'}, + '\xe3\x82\xbdtest': {'name': '\u30bdtest'}, + '5678': {'name': '9876'} + } + + def get(self, resource_id): + try: + return self.resources[str(resource_id)] + except KeyError: + raise ksa_exceptions.NotFound(resource_id) + + def find(self, name=None): + if name == '9999': + # NOTE(morganfainberg): special case that raises NoUniqueMatch. + raise ksc_exceptions.NoUniqueMatch() + for resource_id, resource in self.resources.items(): + if resource['name'] == str(name): + return resource + raise ksa_exceptions.NotFound(name) + + +class FindResourceTestCase(test_utils.TestCase): + + def setUp(self): + super(FindResourceTestCase, self).setUp() + self.manager = FakeManager() + + def test_find_none(self): + self.assertRaises(ksc_exceptions.CommandError, + utils.find_resource, + self.manager, + 'asdf') + + def test_find_by_integer_id(self): + output = utils.find_resource(self.manager, 1234) + self.assertEqual(output, self.manager.resources['1234']) + + def test_find_by_str_id(self): + output = utils.find_resource(self.manager, '1234') + self.assertEqual(output, self.manager.resources['1234']) + + def test_find_by_uuid(self): + uuid = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' + output = utils.find_resource(self.manager, uuid) + self.assertEqual(output, self.manager.resources[uuid]) + + def test_find_by_unicode(self): + name = '\xe3\x82\xbdtest' + output = utils.find_resource(self.manager, name) + self.assertEqual(output, self.manager.resources[name]) + + def test_find_by_str_name(self): + output = utils.find_resource(self.manager, 'entity_one') + self.assertEqual(output, self.manager.resources['1234']) + + def test_find_by_int_name(self): + output = utils.find_resource(self.manager, 9876) + self.assertEqual(output, self.manager.resources['5678']) + + def test_find_no_unique_match(self): + self.assertRaises(ksc_exceptions.CommandError, + utils.find_resource, + self.manager, + 9999) + + +class FakeObject(object): + def __init__(self, name): + self.name = name + + +class HashSignedTokenTestCase(test_utils.TestCase, + testresources.ResourcedTestCase): + """Unit tests for utils.hash_signed_token().""" + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def test_default_md5(self): + """The default hash method is md5.""" + token = self.examples.SIGNED_TOKEN_SCOPED + token = token.encode('utf-8') + token_id_default = utils.hash_signed_token(token) + token_id_md5 = utils.hash_signed_token(token, mode='md5') + self.assertThat(token_id_default, matchers.Equals(token_id_md5)) + # md5 hash is 32 chars. + self.assertThat(token_id_default, matchers.HasLength(32)) + + def test_sha256(self): + """Can also hash with sha256.""" + token = self.examples.SIGNED_TOKEN_SCOPED + token = token.encode('utf-8') + token_id = utils.hash_signed_token(token, mode='sha256') + # sha256 hash is 64 chars. + self.assertThat(token_id, matchers.HasLength(64)) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/utils.py b/keystoneclient/tests/unit/utils.py new file mode 100644 index 000000000..4463213b1 --- /dev/null +++ b/keystoneclient/tests/unit/utils.py @@ -0,0 +1,184 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import sys +import urllib.parse as urlparse +import uuid + +import fixtures +from oslo_serialization import jsonutils +import requests +import requests_mock +from requests_mock.contrib import fixture +import testscenarios +import testtools + +from keystoneclient.tests.unit import client_fixtures + + +class TestCase(testtools.TestCase): + + TEST_DOMAIN_ID = uuid.uuid4().hex + TEST_DOMAIN_NAME = uuid.uuid4().hex + TEST_GROUP_ID = uuid.uuid4().hex + TEST_ROLE_ID = uuid.uuid4().hex + TEST_TENANT_ID = uuid.uuid4().hex + TEST_TENANT_NAME = uuid.uuid4().hex + TEST_TOKEN = uuid.uuid4().hex + TEST_TRUST_ID = uuid.uuid4().hex + TEST_USER = uuid.uuid4().hex + TEST_USER_ID = uuid.uuid4().hex + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + + def setUp(self): + super(TestCase, self).setUp() + self.deprecations = self.useFixture(client_fixtures.Deprecations()) + + self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) + self.requests_mock = self.useFixture(fixture.Fixture()) + + def stub_url(self, method, parts=None, base_url=None, json=None, **kwargs): + if not base_url: + base_url = self.TEST_URL + + if json: + kwargs['text'] = jsonutils.dumps(json) + headers = kwargs.setdefault('headers', {}) + headers['Content-Type'] = 'application/json' + + if parts: + url = '/'.join([p.strip('/') for p in [base_url] + parts]) + else: + url = base_url + + url = url.replace("/?", "?") + self.requests_mock.register_uri(method, url, **kwargs) + + def assertRequestBodyIs(self, body=None, json=None): + last_request_body = self.requests_mock.last_request.body + if json: + val = jsonutils.loads(last_request_body) + self.assertEqual(json, val) + elif body: + self.assertEqual(body, last_request_body) + + def assertQueryStringIs(self, qs=''): + r"""Verify the QueryString matches what is expected. + + The qs parameter should be of the format \'foo=bar&abc=xyz\' + """ + expected = urlparse.parse_qs(qs, keep_blank_values=True) + parts = urlparse.urlparse(self.requests_mock.last_request.url) + querystring = urlparse.parse_qs(parts.query, keep_blank_values=True) + self.assertEqual(expected, querystring) + + def assertQueryStringContains(self, **kwargs): + """Verify the query string contains the expected parameters. + + This method is used to verify that the query string for the most recent + request made contains all the parameters provided as ``kwargs``, and + that the value of each parameter contains the value for the kwarg. If + the value for the kwarg is an empty string (''), then all that's + verified is that the parameter is present. + + """ + parts = urlparse.urlparse(self.requests_mock.last_request.url) + qs = urlparse.parse_qs(parts.query, keep_blank_values=True) + + for k, v in kwargs.items(): + self.assertIn(k, qs) + self.assertIn(v, qs[k]) + + def assertRequestHeaderEqual(self, name, val): + """Verify that the last request made contains a header and its value. + + The request must have already been made. + """ + headers = self.requests_mock.last_request.headers + self.assertEqual(headers.get(name), val) + + +def test_response(**kwargs): + r = requests.Request(method='GET', url='http://localhost:5000').prepare() + return requests_mock.create_response(r, **kwargs) + + +class DisableModuleFixture(fixtures.Fixture): + """A fixture to provide support for unloading/disabling modules.""" + + def __init__(self, module, *args, **kw): + super(DisableModuleFixture, self).__init__(*args, **kw) + self.module = module + self._finders = [] + self._cleared_modules = {} + + def tearDown(self): + super(DisableModuleFixture, self).tearDown() + for finder in self._finders: + sys.meta_path.remove(finder) + sys.modules.update(self._cleared_modules) + + def clear_module(self): + cleared_modules = {} + for fullname in list(sys.modules): + if (fullname == self.module or + fullname.startswith(self.module + '.')): + cleared_modules[fullname] = sys.modules.pop(fullname) + return cleared_modules + + def setUp(self): + """Ensure ImportError for the specified module.""" + super(DisableModuleFixture, self).setUp() + + # Clear 'module' references in sys.modules + self._cleared_modules.update(self.clear_module()) + + finder = NoModuleFinder(self.module) + self._finders.append(finder) + sys.meta_path.insert(0, finder) + + +class ClientTestCaseMixin(testscenarios.WithScenarios): + + client_fixture_class = None + data_fixture_class = None + + def setUp(self): + super(ClientTestCaseMixin, self).setUp() + + self.data_fixture = None + self.client_fixture = None + self.client = None + + if self.client_fixture_class: + fix = self.client_fixture_class(self.requests_mock, + self.deprecations) + self.client_fixture = self.useFixture(fix) + self.client = self.client_fixture.client + self.TEST_USER_ID = self.client_fixture.user_id + + if self.data_fixture_class: + fix = self.data_fixture_class(self.requests_mock) + self.data_fixture = self.useFixture(fix) + + +class NoModuleFinder(object): + """Disallow further imports of 'module'.""" + + def __init__(self, module): + self.module = module + + def find_module(self, fullname, path): + if fullname == self.module or fullname.startswith(self.module + '.'): + raise ImportError diff --git a/keystoneclient/tests/unit/v2_0/__init__.py b/keystoneclient/tests/unit/v2_0/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/keystoneclient/tests/unit/v2_0/client_fixtures.py b/keystoneclient/tests/unit/v2_0/client_fixtures.py new file mode 100644 index 000000000..d3d8bcac3 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/client_fixtures.py @@ -0,0 +1,125 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import uuid + +from keystoneauth1 import fixture + + +def unscoped_token(): + return fixture.V2Token(token_id='3e2813b7ba0b4006840c3825860b86ed', + expires='2012-10-03T16:58:01Z', + user_id='c4da488862bd435c9e6c0275a0d0e49a', + user_name='exampleuser') + + +def project_scoped_token(): + _TENANT_ID = '225da22d3ce34b15877ea70b2a575f58' + + f = fixture.V2Token(token_id='04c7d5ffaeef485f9dc69c06db285bdb', + expires='2012-10-03T16:53:36Z', + tenant_id='225da22d3ce34b15877ea70b2a575f58', + tenant_name='exampleproject', + user_id='c4da488862bd435c9e6c0275a0d0e49a', + user_name='exampleuser', + audit_chain_id=uuid.uuid4().hex) + + f.add_role(id='member_id', name='Member') + + s = f.add_service('volume', 'Volume Service') + s.add_endpoint(public='http://public.com:8776/v1/%s' % _TENANT_ID, + admin='http://admin:8776/v1/%s' % _TENANT_ID, + internal='http://internal:8776/v1/%s' % _TENANT_ID, + region='RegionOne') + + s = f.add_service('image', 'Image Service') + s.add_endpoint(public='http://public.com:9292/v1', + admin='http://admin:9292/v1', + internal='http://internal:9292/v1', + region='RegionOne') + + s = f.add_service('compute', 'Compute Service') + s.add_endpoint(public='http://public.com:8774/v2/%s' % _TENANT_ID, + admin='http://admin:8774/v2/%s' % _TENANT_ID, + internal='http://internal:8774/v2/%s' % _TENANT_ID, + region='RegionOne') + + s = f.add_service('ec2', 'EC2 Service') + s.add_endpoint(public='http://public.com:8773/services/Cloud', + admin='http://admin:8773/services/Admin', + internal='http://internal:8773/services/Cloud', + region='RegionOne') + + s = f.add_service('identity', 'Identity Service') + s.add_endpoint(public='http://public.com:5000/v2.0', + admin='http://admin:35357/v2.0', + internal='http://internal:5000/v2.0', + region='RegionOne') + + return f + + +def auth_response_body(): + f = fixture.V2Token(token_id='ab48a9efdfedb23ty3494', + expires='2010-11-01T03:32:15-05:00', + tenant_id='345', + tenant_name='My Project', + user_id='123', + user_name='jqsmith', + audit_chain_id=uuid.uuid4().hex) + + f.add_role(id='234', name='compute:admin') + role = f.add_role(id='235', name='object-store:admin') + role['tenantId'] = '1' + + s = f.add_service('compute', 'Cloud Servers') + endpoint = s.add_endpoint(public='https://compute.north.host/v1/1234', + internal='https://compute.north.host/v1/1234', + region='North') + endpoint['tenantId'] = '1' + endpoint['versionId'] = '1.0' + endpoint['versionInfo'] = 'https://compute.north.host/v1.0/' + endpoint['versionList'] = 'https://compute.north.host/' + + endpoint = s.add_endpoint(public='https://compute.north.host/v1.1/3456', + internal='https://compute.north.host/v1.1/3456', + region='North') + endpoint['tenantId'] = '2' + endpoint['versionId'] = '1.1' + endpoint['versionInfo'] = 'https://compute.north.host/v1.1/' + endpoint['versionList'] = 'https://compute.north.host/' + + s = f.add_service('object-store', 'Cloud Files') + endpoint = s.add_endpoint(public='https://swift.north.host/v1/blah', + internal='https://swift.north.host/v1/blah', + region='South') + endpoint['tenantId'] = '11' + endpoint['versionId'] = '1.0' + endpoint['versionInfo'] = 'uri' + endpoint['versionList'] = 'uri' + + endpoint = s.add_endpoint(public='https://swift.north.host/v1.1/blah', + internal='https://compute.north.host/v1.1/blah', + region='South') + endpoint['tenantId'] = '2' + endpoint['versionId'] = '1.1' + endpoint['versionInfo'] = 'https://swift.north.host/v1.1/' + endpoint['versionList'] = 'https://swift.north.host/' + + s = f.add_service('image', 'Image Servers') + s.add_endpoint(public='https://image.north.host/v1/', + internal='https://image-internal.north.host/v1/', + region='North') + s.add_endpoint(public='https://image.south.host/v1/', + internal='https://image-internal.south.host/v1/', + region='South') + + return f diff --git a/keystoneclient/tests/unit/v2_0/test_access.py b/keystoneclient/tests/unit/v2_0/test_access.py new file mode 100644 index 000000000..95556aff2 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_access.py @@ -0,0 +1,217 @@ +# 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 fixture +from oslo_utils import timeutils +import testresources + +from keystoneclient import access +from keystoneclient.tests.unit import client_fixtures as token_data +from keystoneclient.tests.unit.v2_0 import client_fixtures +from keystoneclient.tests.unit.v2_0 import utils + + +class AccessInfoTest(utils.TestCase, testresources.ResourcedTestCase): + + resources = [('examples', token_data.EXAMPLES_RESOURCE)] + + def test_building_unscoped_accessinfo(self): + token = client_fixtures.unscoped_token() + auth_ref = access.AccessInfo.factory(body=token) + + self.assertTrue(auth_ref) + self.assertIn('token', auth_ref) + + self.assertEqual(auth_ref.auth_token, + '3e2813b7ba0b4006840c3825860b86ed') + self.assertEqual(auth_ref.username, 'exampleuser') + self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEqual(auth_ref.role_ids, []) + self.assertEqual(auth_ref.role_names, []) + + self.assertIsNone(auth_ref.tenant_name) + self.assertIsNone(auth_ref.tenant_id) + + with self.deprecations.expect_deprecations_here(): + self.assertIsNone(auth_ref.auth_url) + with self.deprecations.expect_deprecations_here(): + self.assertIsNone(auth_ref.management_url) + + with self.deprecations.expect_deprecations_here(): + self.assertFalse(auth_ref.scoped) + self.assertFalse(auth_ref.domain_scoped) + self.assertFalse(auth_ref.project_scoped) + self.assertFalse(auth_ref.trust_scoped) + + self.assertIsNone(auth_ref.project_domain_id) + self.assertIsNone(auth_ref.project_domain_name) + self.assertEqual(auth_ref.user_domain_id, 'default') + self.assertEqual(auth_ref.user_domain_name, 'Default') + + self.assertEqual(auth_ref.expires, token.expires) + self.assertEqual(auth_ref.issued, token.issued) + + self.assertEqual(token.audit_id, auth_ref.audit_id) + self.assertIsNone(auth_ref.audit_chain_id) + self.assertIsNone(token.audit_chain_id) + + def test_will_expire_soon(self): + token = client_fixtures.unscoped_token() + expires = timeutils.utcnow() + datetime.timedelta(minutes=5) + token.expires = expires + auth_ref = access.AccessInfo.factory(body=token) + self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) + self.assertTrue(auth_ref.will_expire_soon(stale_duration=300)) + self.assertFalse(auth_ref.will_expire_soon()) + + def test_building_scoped_accessinfo(self): + token = client_fixtures.project_scoped_token() + auth_ref = access.AccessInfo.factory(body=token) + + self.assertTrue(auth_ref) + self.assertIn('token', auth_ref) + self.assertIn('serviceCatalog', auth_ref) + self.assertTrue(auth_ref['serviceCatalog']) + + self.assertEqual(auth_ref.auth_token, + '04c7d5ffaeef485f9dc69c06db285bdb') + self.assertEqual(auth_ref.username, 'exampleuser') + self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEqual(auth_ref.role_ids, ['member_id']) + self.assertEqual(auth_ref.role_names, ['Member']) + + self.assertEqual(auth_ref.tenant_name, 'exampleproject') + self.assertEqual(auth_ref.tenant_id, + '225da22d3ce34b15877ea70b2a575f58') + + self.assertEqual(auth_ref.tenant_name, auth_ref.project_name) + self.assertEqual(auth_ref.tenant_id, auth_ref.project_id) + + with self.deprecations.expect_deprecations_here(): + self.assertEqual(auth_ref.auth_url, + ('http://public.com:5000/v2.0',)) + with self.deprecations.expect_deprecations_here(): + self.assertEqual(auth_ref.management_url, + ('http://admin:35357/v2.0',)) + + self.assertEqual(auth_ref.project_domain_id, 'default') + self.assertEqual(auth_ref.project_domain_name, 'Default') + self.assertEqual(auth_ref.user_domain_id, 'default') + self.assertEqual(auth_ref.user_domain_name, 'Default') + + with self.deprecations.expect_deprecations_here(): + self.assertTrue(auth_ref.scoped) + self.assertTrue(auth_ref.project_scoped) + self.assertFalse(auth_ref.domain_scoped) + + self.assertEqual(token.audit_id, auth_ref.audit_id) + self.assertEqual(token.audit_chain_id, auth_ref.audit_chain_id) + + def test_diablo_token(self): + diablo_token = self.examples.TOKEN_RESPONSES[ + self.examples.VALID_DIABLO_TOKEN] + auth_ref = access.AccessInfo.factory(body=diablo_token) + + self.assertTrue(auth_ref) + self.assertEqual(auth_ref.username, 'user_name1') + self.assertEqual(auth_ref.project_id, 'tenant_id1') + self.assertEqual(auth_ref.project_name, 'tenant_id1') + self.assertEqual(auth_ref.project_domain_id, 'default') + self.assertEqual(auth_ref.project_domain_name, 'Default') + self.assertEqual(auth_ref.user_domain_id, 'default') + self.assertEqual(auth_ref.user_domain_name, 'Default') + self.assertEqual(auth_ref.role_names, ['role1', 'role2']) + with self.deprecations.expect_deprecations_here(): + self.assertFalse(auth_ref.scoped) + + def test_grizzly_token(self): + grizzly_token = self.examples.TOKEN_RESPONSES[ + self.examples.SIGNED_TOKEN_SCOPED_KEY] + auth_ref = access.AccessInfo.factory(body=grizzly_token) + + self.assertEqual(auth_ref.project_id, 'tenant_id1') + self.assertEqual(auth_ref.project_name, 'tenant_name1') + self.assertEqual(auth_ref.project_domain_id, 'default') + self.assertEqual(auth_ref.project_domain_name, 'Default') + self.assertEqual(auth_ref.user_domain_id, 'default') + self.assertEqual(auth_ref.user_domain_name, 'Default') + self.assertEqual(auth_ref.role_names, ['role1', 'role2']) + + def test_v2_roles(self): + role_id = 'a' + role_name = 'b' + + token = fixture.V2Token() + token.set_scope() + token.add_role(id=role_id, name=role_name) + + auth_ref = access.AccessInfo.factory(body=token) + + self.assertEqual([role_id], auth_ref.role_ids) + self.assertEqual([role_id], auth_ref['metadata']['roles']) + self.assertEqual([role_name], auth_ref.role_names) + self.assertEqual([{'name': role_name}], auth_ref['user']['roles']) + + def test_trusts(self): + user_id = uuid.uuid4().hex + trust_id = uuid.uuid4().hex + + token = fixture.V2Token(user_id=user_id, trust_id=trust_id) + token.set_scope() + token.add_role() + + auth_ref = access.AccessInfo.factory(body=token) + + self.assertEqual(trust_id, auth_ref.trust_id) + self.assertEqual(user_id, auth_ref.trustee_user_id) + + self.assertEqual(trust_id, token['access']['trust']['id']) + + def test_override_auth_token(self): + token = fixture.V2Token() + token.set_scope() + token.add_role() + + new_auth_token = uuid.uuid4().hex + + auth_ref = access.AccessInfo.factory(body=token) + + self.assertEqual(token.token_id, auth_ref.auth_token) + + auth_ref.auth_token = new_auth_token + self.assertEqual(new_auth_token, auth_ref.auth_token) + + del auth_ref.auth_token + self.assertEqual(token.token_id, auth_ref.auth_token) + + def test_override_auth_token_in_factory(self): + token = fixture.V2Token() + token.set_scope() + token.add_role() + + new_auth_token = uuid.uuid4().hex + + auth_ref = access.AccessInfo.factory(body=token, + auth_token=new_auth_token) + + self.assertEqual(new_auth_token, auth_ref.auth_token) + del auth_ref.auth_token + self.assertEqual(token.token_id, auth_ref.auth_token) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/v2_0/test_auth.py b/keystoneclient/tests/unit/v2_0/test_auth.py new file mode 100644 index 000000000..b73352471 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_auth.py @@ -0,0 +1,268 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import datetime + +from oslo_serialization import jsonutils +from oslo_utils import timeutils +from testtools import testcase + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import client + + +class AuthenticateAgainstKeystoneTests(utils.TestCase): + def setUp(self): + super(AuthenticateAgainstKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "access": { + "token": { + "expires": "2999-01-01T00:00:10.000123Z", + "id": self.TEST_TOKEN, + "tenant": { + "id": self.TEST_TENANT_ID + }, + }, + "user": { + "id": self.TEST_USER + }, + "serviceCatalog": self.TEST_SERVICE_CATALOG, + }, + } + self.TEST_REQUEST_BODY = { + "auth": { + "passwordCredentials": { + "username": self.TEST_USER, + "password": self.TEST_TOKEN, + }, + "tenantId": self.TEST_TENANT_ID, + }, + } + + def test_authenticate_success_expired(self): + resp_a = copy.deepcopy(self.TEST_RESPONSE_DICT) + resp_b = copy.deepcopy(self.TEST_RESPONSE_DICT) + headers = {'Content-Type': 'application/json'} + + # Build an expired token + resp_a['access']['token']['expires'] = ( + (timeutils.utcnow() - datetime.timedelta(1)).isoformat()) + + # Build a new response + TEST_TOKEN = "abcdef" + resp_b['access']['token']['expires'] = '2999-01-01T00:00:10.000123Z' + resp_b['access']['token']['id'] = TEST_TOKEN + + # return expired first, and then the new response + self.stub_auth(response_list=[{'json': resp_a, 'headers': headers}, + {'json': resp_b, 'headers': headers}]) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client(project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_TOKEN) + + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + + with self.deprecations.expect_deprecations_here(): + self.assertEqual(cs.auth_token, TEST_TOKEN) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_failure(self): + _auth = 'auth' + _cred = 'passwordCredentials' + _pass = 'password' + self.TEST_REQUEST_BODY[_auth][_cred][_pass] = 'bad_key' + error = {"unauthorized": {"message": "Unauthorized", + "code": "401"}} + + self.stub_auth(status_code=401, json=error) + + with testcase.ExpectedException(exceptions.Unauthorized): + with self.deprecations.expect_deprecations_here(): + client.Client(username=self.TEST_USER, + password="bad_key", + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_auth_redirect(self): + self.stub_auth(status_code=305, text='Use Proxy', + headers={'Location': self.TEST_ADMIN_URL + "/tokens"}) + + self.stub_auth(base_url=self.TEST_ADMIN_URL, + json=self.TEST_RESPONSE_DICT) + + with self.deprecations.expect_deprecations_here(): + cs = client.Client(username=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_password_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + with self.deprecations.expect_deprecations_here(): + cs = client.Client(username=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_password_unscoped(self): + del self.TEST_RESPONSE_DICT['access']['serviceCatalog'] + del self.TEST_REQUEST_BODY['auth']['tenantId'] + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + with self.deprecations.expect_deprecations_here(): + cs = client.Client(username=self.TEST_USER, + password=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertNotIn('serviceCatalog', cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_auth_url_token_authentication(self): + fake_token = 'fake_token' + fake_url = '/fake-url' + fake_resp = {'result': True} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [fake_url], json=fake_resp, + base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT) + + with self.deprecations.expect_deprecations_here(): + cl = client.Client(auth_url=self.TEST_URL, + token=fake_token) + json_body = jsonutils.loads(self.requests_mock.last_request.body) + self.assertEqual(json_body['auth']['token']['id'], fake_token) + + with self.deprecations.expect_deprecations_here(): + resp, body = cl.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests_mock.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) + + def test_authenticate_success_token_scoped(self): + del self.TEST_REQUEST_BODY['auth']['passwordCredentials'] + self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + with self.deprecations.expect_deprecations_here(): + cs = client.Client(token=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_token_scoped_trust(self): + del self.TEST_REQUEST_BODY['auth']['passwordCredentials'] + self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} + self.TEST_REQUEST_BODY['auth']['trust_id'] = self.TEST_TRUST_ID + response = self.TEST_RESPONSE_DICT.copy() + response['access']['trust'] = {"trustee_user_id": self.TEST_USER, + "id": self.TEST_TRUST_ID} + self.stub_auth(json=response) + + with self.deprecations.expect_deprecations_here(): + cs = client.Client(token=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + trust_id=self.TEST_TRUST_ID, + auth_url=self.TEST_URL) + self.assertTrue(cs.auth_ref.trust_scoped) + self.assertEqual(cs.auth_ref.trust_id, self.TEST_TRUST_ID) + self.assertEqual(cs.auth_ref.trustee_user_id, self.TEST_USER) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_token_unscoped(self): + del self.TEST_REQUEST_BODY['auth']['passwordCredentials'] + del self.TEST_REQUEST_BODY['auth']['tenantId'] + del self.TEST_RESPONSE_DICT['access']['serviceCatalog'] + self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + with self.deprecations.expect_deprecations_here(): + cs = client.Client(token=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertNotIn('serviceCatalog', cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_allow_override_of_auth_token(self): + fake_url = '/fake-url' + fake_token = 'fake_token' + fake_resp = {'result': True} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [fake_url], json=fake_resp, + base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT) + + with self.deprecations.expect_deprecations_here(): + cl = client.Client(username='exampleuser', + password='password', + project_name='exampleproject', + auth_url=self.TEST_URL) + + self.assertEqual(cl.auth_token, self.TEST_TOKEN) + + # the token returned from the authentication will be used + resp, body = cl._adapter.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests_mock.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) + + # then override that token and the new token shall be used + cl.auth_token = fake_token + + resp, body = cl._adapter.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests_mock.last_request.headers.get('X-Auth-Token') + self.assertEqual(fake_token, token) + + # if we clear that overridden token then we fall back to the original + del cl.auth_token + + resp, body = cl._adapter.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests_mock.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) diff --git a/keystoneclient/tests/unit/v2_0/test_certificates.py b/keystoneclient/tests/unit/v2_0/test_certificates.py new file mode 100644 index 000000000..4f689d9ad --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_certificates.py @@ -0,0 +1,40 @@ +# Copyright 2014 IBM Corp. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import testresources + +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit.v2_0 import utils + + +class CertificateTests(utils.ClientTestCase, testresources.ResourcedTestCase): + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def test_get_ca_certificate(self): + self.stub_url('GET', ['certificates', 'ca'], + headers={'Content-Type': 'text/html; charset=UTF-8'}, + text=self.examples.SIGNING_CA) + res = self.client.certificates.get_ca_certificate() + self.assertEqual(self.examples.SIGNING_CA, res) + + def test_get_signing_certificate(self): + self.stub_url('GET', ['certificates', 'signing'], + headers={'Content-Type': 'text/html; charset=UTF-8'}, + text=self.examples.SIGNING_CERT) + res = self.client.certificates.get_signing_certificate() + self.assertEqual(self.examples.SIGNING_CERT, res) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/v2_0/test_client.py b/keystoneclient/tests/unit/v2_0/test_client.py new file mode 100644 index 000000000..7fe9b1813 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_client.py @@ -0,0 +1,221 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from oslo_serialization import jsonutils + +from keystoneauth1 import fixture + +from keystoneauth1 import session as auth_session +from keystoneclient.auth import token_endpoint +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit.v2_0 import client_fixtures +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import client + + +class KeystoneClientTest(utils.TestCase): + + def test_unscoped_init(self): + token = client_fixtures.unscoped_token() + self.stub_auth(json=token) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + c = client.Client(username='exampleuser', + password='password', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + with self.deprecations.expect_deprecations_here(): + self.assertFalse(c.auth_ref.scoped) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertIsNone(c.auth_ref.trust_id) + self.assertFalse(c.auth_ref.trust_scoped) + self.assertIsNone(c.get_project_id(session=None)) + self.assertEqual(token.user_id, c.get_user_id(session=None)) + + def test_scoped_init(self): + token = client_fixtures.project_scoped_token() + self.stub_auth(json=token) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + c = client.Client(username='exampleuser', + password='password', + project_name='exampleproject', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + with self.deprecations.expect_deprecations_here(): + self.assertTrue(c.auth_ref.scoped) + self.assertTrue(c.auth_ref.project_scoped) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertIsNone(c.auth_ref.trust_id) + self.assertFalse(c.auth_ref.trust_scoped) + + self.assertEqual(token.tenant_id, c.get_project_id(session=None)) + self.assertEqual(token.user_id, c.get_user_id(session=None)) + + def test_auth_ref_load(self): + self.stub_auth(json=client_fixtures.project_scoped_token()) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = client.Client(username='exampleuser', + password='password', + project_name='exampleproject', + auth_url=self.TEST_URL) + cache = jsonutils.dumps(cl.auth_ref) + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + new_client = client.Client(auth_ref=jsonutils.loads(cache)) + self.assertIsNotNone(new_client.auth_ref) + with self.deprecations.expect_deprecations_here(): + self.assertTrue(new_client.auth_ref.scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertIsNone(new_client.auth_ref.trust_id) + self.assertFalse(new_client.auth_ref.trust_scoped) + self.assertEqual(new_client.username, 'exampleuser') + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v2.0') + + def test_auth_ref_load_with_overridden_arguments(self): + self.stub_auth(json=client_fixtures.project_scoped_token()) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = client.Client(username='exampleuser', + password='password', + project_name='exampleproject', + auth_url=self.TEST_URL) + cache = jsonutils.dumps(cl.auth_ref) + new_auth_url = "http://new-public:5000/v2.0" + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + new_client = client.Client(auth_ref=jsonutils.loads(cache), + auth_url=new_auth_url) + self.assertIsNotNone(new_client.auth_ref) + with self.deprecations.expect_deprecations_here(): + self.assertTrue(new_client.auth_ref.scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertIsNone(new_client.auth_ref.trust_id) + self.assertFalse(new_client.auth_ref.trust_scoped) + self.assertEqual(new_client.auth_url, new_auth_url) + self.assertEqual(new_client.username, 'exampleuser') + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v2.0') + + def test_init_err_no_auth_url(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + self.assertRaises(exceptions.AuthorizationFailure, + client.Client, + username='exampleuser', + password='password') + + def test_management_url_is_updated(self): + first = fixture.V2Token() + first.set_scope() + admin_url = 'http://admin:35357/v2.0' + second_url = 'http://secondurl:35357/v2.0' + + s = first.add_service('identity') + s.add_endpoint(public='http://public.com:5000/v2.0', + admin=admin_url) + + second = fixture.V2Token() + second.set_scope() + s = second.add_service('identity') + s.add_endpoint(public='http://secondurl:5000/v2.0', + admin=second_url) + + self.stub_auth(response_list=[{'json': first}, {'json': second}]) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = client.Client(username='exampleuser', + password='password', + project_name='exampleproject', + auth_url=self.TEST_URL) + self.assertEqual(cl.management_url, admin_url) + + with self.deprecations.expect_deprecations_here(): + cl.authenticate() + self.assertEqual(cl.management_url, second_url) + + def test_client_with_region_name_passes_to_service_catalog(self): + # NOTE(jamielennox): this is deprecated behaviour that should be + # removed ASAP, however must remain compatible. + self.stub_auth(json=client_fixtures.auth_response_body()) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = client.Client(username='exampleuser', + password='password', + project_name='exampleproject', + auth_url=self.TEST_URL, + region_name='North') + self.assertEqual(cl.service_catalog.url_for(service_type='image'), + 'https://image.north.host/v1/') + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = client.Client(username='exampleuser', + password='password', + project_name='exampleproject', + auth_url=self.TEST_URL, + region_name='South') + self.assertEqual(cl.service_catalog.url_for(service_type='image'), + 'https://image.south.host/v1/') + + def test_client_without_auth_params(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + self.assertRaises(exceptions.AuthorizationFailure, + client.Client, + project_name='exampleproject', + auth_url=self.TEST_URL) + + def test_client_params(self): + with self.deprecations.expect_deprecations_here(): + sess = session.Session() + auth = token_endpoint.Token('a', 'b') + + opts = {'auth': auth, + 'connect_retries': 50, + 'endpoint_override': uuid.uuid4().hex, + 'interface': uuid.uuid4().hex, + 'region_name': uuid.uuid4().hex, + 'service_name': uuid.uuid4().hex, + 'user_agent': uuid.uuid4().hex, + } + + cl = client.Client(session=sess, **opts) + + for k, v in opts.items(): + self.assertEqual(v, getattr(cl._adapter, k)) + + self.assertEqual('identity', cl._adapter.service_type) + self.assertEqual((2, 0), cl._adapter.version) + + def test_empty_service_catalog_param(self): + # Client().service_catalog should return None if the client is not + # authenticated + sess = auth_session.Session() + cl = client.Client(session=sess) + self.assertIsNone(cl.service_catalog) diff --git a/tests/v2_0/test_discovery.py b/keystoneclient/tests/unit/v2_0/test_discovery.py similarity index 62% rename from tests/v2_0/test_discovery.py rename to keystoneclient/tests/unit/v2_0/test_discovery.py index 06f210744..a3700e0e1 100644 --- a/tests/v2_0/test_discovery.py +++ b/keystoneclient/tests/unit/v2_0/test_discovery.py @@ -1,9 +1,18 @@ -import copy -import json -import requests +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + from keystoneclient.generic import client -from tests import utils +from keystoneclient.tests.unit.v2_0 import utils class DiscoverKeystoneTests(utils.UnauthenticatedTestCase): @@ -20,11 +29,11 @@ def setUp(self): "href": "http://127.0.0.1:5000/v2.0/", }, {"rel": "describedby", "type": "text/html", - "href": "http://docs.openstack.org/api/" + "href": "https://docs.openstack.org/api/" "openstack-identity-service/2.0/content/", }, {"rel": "describedby", "type": "application/pdf", - "href": "http://docs.openstack.org/api/" + "href": "https://docs.openstack.org/api/" "openstack-identity-service/2.0/" "identity-dev-guide-2.0.pdf", }, {"rel": "describedby", @@ -41,52 +50,35 @@ def setUp(self): }], }, } - self.TEST_REQUEST_HEADERS = { - 'User-Agent': 'python-keystoneclient', - 'Accept': 'application/json', - } def test_get_versions(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_RESPONSE_DICT), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - self.TEST_ROOT_URL, - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() + self.stub_url('GET', base_url=self.TEST_ROOT_URL, + json=self.TEST_RESPONSE_DICT) - cs = client.Client() + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client() versions = cs.discover(self.TEST_ROOT_URL) self.assertIsInstance(versions, dict) self.assertIn('message', versions) self.assertIn('v2.0', versions) - self.assertEquals( + self.assertEqual( versions['v2.0']['url'], self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0] ['href']) def test_get_version_local(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_RESPONSE_DICT), - }) - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - "http://localhost:35357", - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() + self.stub_url('GET', base_url="http://localhost:35357/", + json=self.TEST_RESPONSE_DICT) - cs = client.Client() + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client() versions = cs.discover() self.assertIsInstance(versions, dict) self.assertIn('message', versions) self.assertIn('v2.0', versions) - self.assertEquals( + self.assertEqual( versions['v2.0']['url'], self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0] ['href']) diff --git a/keystoneclient/tests/unit/v2_0/test_ec2.py b/keystoneclient/tests/unit/v2_0/test_ec2.py new file mode 100644 index 000000000..3df053de1 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_ec2.py @@ -0,0 +1,107 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import ec2 + + +class EC2Tests(utils.ClientTestCase): + + def test_create(self): + user_id = 'usr' + tenant_id = 'tnt' + req_body = { + "tenant_id": tenant_id, + } + resp_body = { + "credential": { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + } + } + self.stub_url('POST', ['users', user_id, 'credentials', + 'OS-EC2'], json=resp_body) + + cred = self.client.ec2.create(user_id, tenant_id) + self.assertIsInstance(cred, ec2.EC2) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + self.assertRequestBodyIs(json=req_body) + + def test_get(self): + user_id = 'usr' + tenant_id = 'tnt' + resp_body = { + "credential": { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + } + } + self.stub_url('GET', ['users', user_id, 'credentials', + 'OS-EC2', 'access'], json=resp_body) + + cred = self.client.ec2.get(user_id, 'access') + self.assertIsInstance(cred, ec2.EC2) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + + def test_list(self): + user_id = 'usr' + tenant_id = 'tnt' + resp_body = { + "credentials": { + "values": [ + { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + }, + { + "access": "another", + "secret": "key", + "tenant_id": tenant_id, + "created": "12/12/31", + "enabled": True, + } + ] + } + } + self.stub_url('GET', ['users', user_id, 'credentials', + 'OS-EC2'], json=resp_body) + + creds = self.client.ec2.list(user_id) + self.assertEqual(len(creds), 2) + cred = creds[0] + self.assertIsInstance(cred, ec2.EC2) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + + def test_delete(self): + user_id = 'usr' + access = 'access' + self.stub_url('DELETE', ['users', user_id, 'credentials', + 'OS-EC2', access], status_code=204) + self.client.ec2.delete(user_id, access) diff --git a/keystoneclient/tests/unit/v2_0/test_endpoints.py b/keystoneclient/tests/unit/v2_0/test_endpoints.py new file mode 100644 index 000000000..7b15cccc9 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_endpoints.py @@ -0,0 +1,147 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import endpoints + + +class EndpointTests(utils.ClientTestCase): + def setUp(self): + super(EndpointTests, self).setUp() + self.TEST_ENDPOINTS = { + 'endpoints': [ + { + 'adminurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'id': '8f9531231e044e218824b0e58688d262', + 'internalurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'publicurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'region': 'RegionOne', + }, + { + 'adminurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'id': '8f9531231e044e218824b0e58688d263', + 'internalurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'publicurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'region': 'RegionOne', + } + ] + } + + def test_create_with_optional_params(self): + req_body = { + "endpoint": { + "region": "RegionOne", + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "internalurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "service_id": uuid.uuid4().hex, + } + } + + resp_body = { + "endpoint": { + "adminurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "region": "RegionOne", + "id": uuid.uuid4().hex, + "internalurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + } + } + + self.stub_url('POST', ['endpoints'], json=resp_body) + + endpoint = self.client.endpoints.create( + region=req_body['endpoint']['region'], + publicurl=req_body['endpoint']['publicurl'], + adminurl=req_body['endpoint']['adminurl'], + internalurl=req_body['endpoint']['internalurl'], + service_id=req_body['endpoint']['service_id'] + ) + self.assertIsInstance(endpoint, endpoints.Endpoint) + self.assertRequestBodyIs(json=req_body) + + def test_create_with_optional_params_as_none(self): + req_body_without_defaults = { + "endpoint": { + "region": "RegionOne", + "service_id": uuid.uuid4().hex, + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": None, + "internalurl": None, + } + } + + resp_body = { + "endpoint": { + "region": "RegionOne", + "id": uuid.uuid4().hex, + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": None, + "internalurl": None, + } + } + + self.stub_url('POST', ['endpoints'], json=resp_body) + + endpoint_without_defaults = self.client.endpoints.create( + region=req_body_without_defaults['endpoint']['region'], + publicurl=req_body_without_defaults['endpoint']['publicurl'], + service_id=req_body_without_defaults['endpoint']['service_id'], + adminurl=None, + internalurl=None + ) + self.assertIsInstance(endpoint_without_defaults, endpoints.Endpoint) + self.assertRequestBodyIs(json=req_body_without_defaults) + + def test_create_without_optional_params(self): + req_body_without_defaults = { + "endpoint": { + "region": "RegionOne", + "service_id": uuid.uuid4().hex, + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": None, + "internalurl": None, + } + } + + resp_body = { + "endpoint": { + "region": "RegionOne", + "id": uuid.uuid4().hex, + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": None, + "internalurl": None, + } + } + + self.stub_url('POST', ['endpoints'], json=resp_body) + + endpoint_without_defaults = self.client.endpoints.create( + region=req_body_without_defaults['endpoint']['region'], + publicurl=req_body_without_defaults['endpoint']['publicurl'], + service_id=req_body_without_defaults['endpoint']['service_id'] + ) + self.assertIsInstance(endpoint_without_defaults, endpoints.Endpoint) + self.assertRequestBodyIs(json=req_body_without_defaults) + + def test_delete(self): + self.stub_url('DELETE', ['endpoints', '8f953'], status_code=204) + self.client.endpoints.delete('8f953') + + def test_list(self): + self.stub_url('GET', ['endpoints'], json=self.TEST_ENDPOINTS) + + endpoint_list = self.client.endpoints.list() + [self.assertIsInstance(r, endpoints.Endpoint) + for r in endpoint_list] diff --git a/keystoneclient/tests/unit/v2_0/test_extensions.py b/keystoneclient/tests/unit/v2_0/test_extensions.py new file mode 100644 index 000000000..3927bc07c --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_extensions.py @@ -0,0 +1,63 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import extensions + + +class ExtensionTests(utils.ClientTestCase): + def setUp(self): + super(ExtensionTests, self).setUp() + self.TEST_EXTENSIONS = { + 'extensions': { + "values": [ + { + 'name': 'OpenStack Keystone User CRUD', + 'namespace': 'https://docs.openstack.org/' + 'identity/api/ext/OS-KSCRUD/v1.0', + 'updated': '2013-07-07T12:00:0-00:00', + 'alias': 'OS-KSCRUD', + 'description': + 'OpenStack extensions to Keystone v2.0 API' + ' enabling User Operations.', + 'links': + '[{"href":' + '"https://github.com/openstack/identity-api", "type":' + ' "text/html", "rel": "describedby"}]', + }, + { + 'name': 'OpenStack EC2 API', + 'namespace': 'https://docs.openstack.org/' + 'identity/api/ext/OS-EC2/v1.0', + 'updated': '2013-09-07T12:00:0-00:00', + 'alias': 'OS-EC2', + 'description': 'OpenStack EC2 Credentials backend.', + 'links': '[{"href":' + '"https://github.com/openstack/identity-api", "type":' + ' "text/html", "rel": "describedby"}]', + } + ] + } + } + + def test_list(self): + self.stub_url('GET', ['extensions'], json=self.TEST_EXTENSIONS) + extensions_list = self.client.extensions.list() + self.assertEqual(2, len(extensions_list)) + for extension in extensions_list: + self.assertIsInstance(extension, extensions.Extension) + self.assertIsNotNone(extension.alias) + self.assertIsNotNone(extension.description) + self.assertIsNotNone(extension.links) + self.assertIsNotNone(extension.name) + self.assertIsNotNone(extension.namespace) + self.assertIsNotNone(extension.updated) diff --git a/keystoneclient/tests/unit/v2_0/test_roles.py b/keystoneclient/tests/unit/v2_0/test_roles.py new file mode 100644 index 000000000..fab21383c --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_roles.py @@ -0,0 +1,121 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import roles + + +class RoleTests(utils.ClientTestCase): + def setUp(self): + super(RoleTests, self).setUp() + + self.ADMIN_ROLE_ID = uuid.uuid4().hex + self.MEMBER_ROLE_ID = uuid.uuid4().hex + + self.TEST_ROLES = { + "roles": { + "values": [ + { + "name": "admin", + "id": self.ADMIN_ROLE_ID, + }, + { + "name": "member", + "id": self.MEMBER_ROLE_ID, + } + ], + }, + } + + def test_create(self): + req_body = { + "role": { + "name": "sysadmin", + } + } + role_id = uuid.uuid4().hex + resp_body = { + "role": { + "name": "sysadmin", + "id": role_id, + } + } + self.stub_url('POST', ['OS-KSADM', 'roles'], json=resp_body) + + role = self.client.roles.create(req_body['role']['name']) + self.assertRequestBodyIs(json=req_body) + self.assertIsInstance(role, roles.Role) + self.assertEqual(role.id, role_id) + self.assertEqual(role.name, req_body['role']['name']) + + def test_delete(self): + self.stub_url('DELETE', + ['OS-KSADM', 'roles', self.ADMIN_ROLE_ID], + status_code=204) + self.client.roles.delete(self.ADMIN_ROLE_ID) + + def test_get(self): + self.stub_url('GET', ['OS-KSADM', 'roles', self.ADMIN_ROLE_ID], + json={'role': self.TEST_ROLES['roles']['values'][0]}) + + role = self.client.roles.get(self.ADMIN_ROLE_ID) + self.assertIsInstance(role, roles.Role) + self.assertEqual(role.id, self.ADMIN_ROLE_ID) + self.assertEqual(role.name, 'admin') + + def test_list(self): + self.stub_url('GET', ['OS-KSADM', 'roles'], + json=self.TEST_ROLES) + + role_list = self.client.roles.list() + [self.assertIsInstance(r, roles.Role) for r in role_list] + + def test_roles_for_user(self): + self.stub_url('GET', ['users', 'foo', 'roles'], + json=self.TEST_ROLES) + + role_list = self.client.roles.roles_for_user('foo') + [self.assertIsInstance(r, roles.Role) for r in role_list] + + def test_roles_for_user_tenant(self): + self.stub_url('GET', ['tenants', 'barrr', 'users', 'foo', + 'roles'], json=self.TEST_ROLES) + + role_list = self.client.roles.roles_for_user('foo', 'barrr') + [self.assertIsInstance(r, roles.Role) for r in role_list] + + def test_add_user_role(self): + self.stub_url('PUT', ['users', 'foo', 'roles', 'OS-KSADM', + 'barrr'], status_code=204) + + self.client.roles.add_user_role('foo', 'barrr') + + def test_add_user_role_tenant(self): + id_ = uuid.uuid4().hex + self.stub_url('PUT', ['tenants', id_, 'users', 'foo', 'roles', + 'OS-KSADM', 'barrr'], status_code=204) + + self.client.roles.add_user_role('foo', 'barrr', id_) + + def test_remove_user_role(self): + self.stub_url('DELETE', ['users', 'foo', 'roles', 'OS-KSADM', + 'barrr'], status_code=204) + self.client.roles.remove_user_role('foo', 'barrr') + + def test_remove_user_role_tenant(self): + id_ = uuid.uuid4().hex + self.stub_url('DELETE', ['tenants', id_, 'users', 'foo', + 'roles', 'OS-KSADM', 'barrr'], + status_code=204) + self.client.roles.remove_user_role('foo', 'barrr', id_) diff --git a/keystoneclient/tests/unit/v2_0/test_service_catalog.py b/keystoneclient/tests/unit/v2_0/test_service_catalog.py new file mode 100644 index 000000000..612b7f0a7 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_service_catalog.py @@ -0,0 +1,207 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneauth1 import fixture + +from keystoneclient import access +from keystoneclient import exceptions +from keystoneclient.tests.unit.v2_0 import client_fixtures +from keystoneclient.tests.unit.v2_0 import utils + + +class ServiceCatalogTest(utils.TestCase): + def setUp(self): + super(ServiceCatalogTest, self).setUp() + self.AUTH_RESPONSE_BODY = client_fixtures.auth_response_body() + + def test_building_a_service_catalog(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + self.assertEqual(sc.url_for(service_type='compute'), + "https://compute.north.host/v1/1234") + self.assertEqual(sc.url_for('tenantId', '1', service_type='compute'), + "https://compute.north.host/v1/1234") + self.assertEqual(sc.url_for('tenantId', '2', service_type='compute'), + "https://compute.north.host/v1.1/3456") + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", + "South", service_type='compute') + + def test_service_catalog_endpoints(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + public_ep = sc.get_endpoints(service_type='compute', + endpoint_type='publicURL') + self.assertEqual(public_ep['compute'][1]['tenantId'], '2') + self.assertEqual(public_ep['compute'][1]['versionId'], '1.1') + self.assertEqual(public_ep['compute'][1]['internalURL'], + "https://compute.north.host/v1.1/3456") + + def test_service_catalog_regions(self): + self.AUTH_RESPONSE_BODY['access']['region_name'] = "North" + # Setting region_name on the catalog is deprecated. + with self.deprecations.expect_deprecations_here(): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', endpoint_type='publicURL') + self.assertEqual(url, "https://image.north.host/v1/") + + self.AUTH_RESPONSE_BODY['access']['region_name'] = "South" + # Setting region_name on the catalog is deprecated. + with self.deprecations.expect_deprecations_here(): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', endpoint_type='internalURL') + self.assertEqual(url, "https://image-internal.south.host/v1/") + + def test_service_catalog_empty(self): + self.AUTH_RESPONSE_BODY['access']['serviceCatalog'] = [] + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + self.assertRaises(exceptions.EmptyCatalog, + auth_ref.service_catalog.url_for, + service_type='image', + endpoint_type='internalURL') + + def test_service_catalog_get_endpoints_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + endpoints = sc.get_endpoints(service_type='image', region_name='North') + self.assertEqual(len(endpoints), 1) + self.assertEqual(endpoints['image'][0]['publicURL'], + 'https://image.north.host/v1/') + + endpoints = sc.get_endpoints(service_type='image', region_name='South') + self.assertEqual(len(endpoints), 1) + self.assertEqual(endpoints['image'][0]['publicURL'], + 'https://image.south.host/v1/') + + endpoints = sc.get_endpoints(service_type='compute') + self.assertEqual(len(endpoints['compute']), 2) + + endpoints = sc.get_endpoints(service_type='compute', + region_name='North') + self.assertEqual(len(endpoints['compute']), 2) + + endpoints = sc.get_endpoints(service_type='compute', + region_name='West') + self.assertEqual(len(endpoints['compute']), 0) + + def test_service_catalog_url_for_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', region_name='North') + self.assertEqual(url, 'https://image.north.host/v1/') + + url = sc.url_for(service_type='image', region_name='South') + self.assertEqual(url, 'https://image.south.host/v1/') + + url = sc.url_for(service_type='compute', + region_name='North', + attr='versionId', + filter_value='1.1') + self.assertEqual(url, 'https://compute.north.host/v1.1/3456') + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, + service_type='image', region_name='West') + + def test_servcie_catalog_get_url_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + urls = sc.get_urls(service_type='image') + self.assertEqual(len(urls), 2) + + urls = sc.get_urls(service_type='image', region_name='North') + self.assertEqual(len(urls), 1) + self.assertEqual(urls[0], 'https://image.north.host/v1/') + + urls = sc.get_urls(service_type='image', region_name='South') + self.assertEqual(len(urls), 1) + self.assertEqual(urls[0], 'https://image.south.host/v1/') + + urls = sc.get_urls(service_type='image', region_name='West') + self.assertIsNone(urls) + + def test_service_catalog_param_overrides_body_region(self): + self.AUTH_RESPONSE_BODY['access']['region_name'] = "North" + # Setting region_name on the catalog is deprecated. + with self.deprecations.expect_deprecations_here(): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image') + self.assertEqual(url, 'https://image.north.host/v1/') + + url = sc.url_for(service_type='image', region_name='South') + self.assertEqual(url, 'https://image.south.host/v1/') + + endpoints = sc.get_endpoints(service_type='image') + self.assertEqual(len(endpoints['image']), 1) + self.assertEqual(endpoints['image'][0]['publicURL'], + 'https://image.north.host/v1/') + + endpoints = sc.get_endpoints(service_type='image', region_name='South') + self.assertEqual(len(endpoints['image']), 1) + self.assertEqual(endpoints['image'][0]['publicURL'], + 'https://image.south.host/v1/') + + def test_service_catalog_service_name(self): + auth_ref = access.AccessInfo.factory(resp=None, + body=self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_name='Image Servers', endpoint_type='public', + service_type='image', region_name='North') + self.assertEqual('https://image.north.host/v1/', url) + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, + service_name='Image Servers', service_type='compute') + + urls = sc.get_urls(service_type='image', service_name='Image Servers', + endpoint_type='public') + + self.assertIn('https://image.north.host/v1/', urls) + self.assertIn('https://image.south.host/v1/', urls) + + urls = sc.get_urls(service_type='image', service_name='Servers', + endpoint_type='public') + + self.assertIsNone(urls) + + def test_service_catalog_multiple_service_types(self): + token = fixture.V2Token() + token.set_scope() + + for i in range(3): + s = token.add_service('compute') + s.add_endpoint(public='public-%d' % i, + admin='admin-%d' % i, + internal='internal-%d' % i, + region='region-%d' % i) + + auth_ref = access.AccessInfo.factory(resp=None, body=token) + + urls = auth_ref.service_catalog.get_urls(service_type='compute', + endpoint_type='publicURL') + + self.assertEqual(set(['public-0', 'public-1', 'public-2']), set(urls)) + + urls = auth_ref.service_catalog.get_urls(service_type='compute', + endpoint_type='publicURL', + region_name='region-1') + + self.assertEqual(('public-1', ), urls) diff --git a/keystoneclient/tests/unit/v2_0/test_services.py b/keystoneclient/tests/unit/v2_0/test_services.py new file mode 100644 index 000000000..72c547699 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_services.py @@ -0,0 +1,129 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import services + + +class ServiceTests(utils.ClientTestCase): + def setUp(self): + super(ServiceTests, self).setUp() + + self.NOVA_SERVICE_ID = uuid.uuid4().hex + self.KEYSTONE_SERVICE_ID = uuid.uuid4().hex + + self.TEST_SERVICES = { + "OS-KSADM:services": { + "values": [ + { + "name": "nova", + "type": "compute", + "description": "Nova-compatible service.", + "id": self.NOVA_SERVICE_ID + }, + { + "name": "keystone", + "type": "identity", + "description": "Keystone-compatible service.", + "id": self.KEYSTONE_SERVICE_ID + }, + ], + }, + } + + def test_create_with_description(self): + req_body = { + "OS-KSADM:service": { + "name": "swift", + "type": "object-store", + "description": "Swift-compatible service.", + } + } + service_id = uuid.uuid4().hex + resp_body = { + "OS-KSADM:service": { + "name": "swift", + "type": "object-store", + "description": "Swift-compatible service.", + "id": service_id, + } + } + self.stub_url('POST', ['OS-KSADM', 'services'], json=resp_body) + + service = self.client.services.create( + req_body['OS-KSADM:service']['name'], + req_body['OS-KSADM:service']['type'], + req_body['OS-KSADM:service']['description']) + self.assertIsInstance(service, services.Service) + self.assertEqual(service.id, service_id) + self.assertEqual(service.name, req_body['OS-KSADM:service']['name']) + self.assertEqual(service.description, + req_body['OS-KSADM:service']['description']) + self.assertRequestBodyIs(json=req_body) + + def test_create_without_description(self): + req_body = { + "OS-KSADM:service": { + "name": "swift", + "type": "object-store", + "description": None, + } + } + service_id = uuid.uuid4().hex + resp_body = { + "OS-KSADM:service": { + "name": "swift", + "type": "object-store", + "id": service_id, + "description": None, + } + } + self.stub_url('POST', ['OS-KSADM', 'services'], json=resp_body) + + service = self.client.services.create( + req_body['OS-KSADM:service']['name'], + req_body['OS-KSADM:service']['type'], + req_body['OS-KSADM:service']['description']) + self.assertIsInstance(service, services.Service) + self.assertEqual(service.id, service_id) + self.assertEqual(service.name, req_body['OS-KSADM:service']['name']) + self.assertIsNone(service.description) + self.assertRequestBodyIs(json=req_body) + + def test_delete(self): + self.stub_url('DELETE', + ['OS-KSADM', 'services', self.NOVA_SERVICE_ID], + status_code=204) + + self.client.services.delete(self.NOVA_SERVICE_ID) + + def test_get(self): + test_services = self.TEST_SERVICES['OS-KSADM:services']['values'][0] + + self.stub_url('GET', ['OS-KSADM', 'services', self.NOVA_SERVICE_ID], + json={'OS-KSADM:service': test_services}) + + service = self.client.services.get(self.NOVA_SERVICE_ID) + self.assertIsInstance(service, services.Service) + self.assertEqual(service.id, self.NOVA_SERVICE_ID) + self.assertEqual(service.name, 'nova') + self.assertEqual(service.type, 'compute') + + def test_list(self): + self.stub_url('GET', ['OS-KSADM', 'services'], + json=self.TEST_SERVICES) + + service_list = self.client.services.list() + [self.assertIsInstance(r, services.Service) + for r in service_list] diff --git a/keystoneclient/tests/unit/v2_0/test_tenants.py b/keystoneclient/tests/unit/v2_0/test_tenants.py new file mode 100644 index 000000000..5e78aa3be --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_tenants.py @@ -0,0 +1,366 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1 import exceptions +from keystoneauth1 import fixture + +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import client +from keystoneclient.v2_0 import tenants +from keystoneclient.v2_0 import users + + +class TenantTests(utils.ClientTestCase): + def setUp(self): + super(TenantTests, self).setUp() + + self.INVIS_ID = uuid.uuid4().hex + self.DEMO_ID = uuid.uuid4().hex + self.ADMIN_ID = uuid.uuid4().hex + self.EXTRAS_ID = uuid.uuid4().hex + + self.TEST_TENANTS = { + "tenants": { + "values": [ + { + "enabled": True, + "description": "A description change!", + "name": "invisible_to_admin", + "id": self.INVIS_ID, + }, + { + "enabled": True, + "description": "None", + "name": "demo", + "id": self.DEMO_ID, + }, + { + "enabled": True, + "description": "None", + "name": "admin", + "id": self.ADMIN_ID, + }, + { + "extravalue01": "metadata01", + "enabled": True, + "description": "For testing extras", + "name": "test_extras", + "id": self.EXTRAS_ID, + } + ], + "links": [], + }, + } + + def test_create(self): + req_body = { + "tenant": { + "name": "tenantX", + "description": "Like tenant 9, but better.", + "enabled": True, + "extravalue01": "metadata01", + }, + } + id_ = uuid.uuid4().hex + resp_body = { + "tenant": { + "name": "tenantX", + "enabled": True, + "id": id_, + "description": "Like tenant 9, but better.", + "extravalue01": "metadata01", + } + } + self.stub_url('POST', ['tenants'], json=resp_body) + + tenant = self.client.tenants.create( + req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled'], + extravalue01=req_body['tenant']['extravalue01'], + name="don't overwrite priors") + self.assertIsInstance(tenant, tenants.Tenant) + self.assertEqual(tenant.id, id_) + self.assertEqual(tenant.name, "tenantX") + self.assertEqual(tenant.description, "Like tenant 9, but better.") + self.assertEqual(tenant.extravalue01, "metadata01") + self.assertRequestBodyIs(json=req_body) + + def test_duplicate_create(self): + req_body = { + "tenant": { + "name": "tenantX", + "description": "The duplicate tenant.", + "enabled": True + }, + } + resp_body = { + "error": { + "message": "Conflict occurred attempting to store project.", + "code": 409, + "title": "Conflict", + } + } + self.stub_url('POST', ['tenants'], status_code=409, json=resp_body) + + def create_duplicate_tenant(): + self.client.tenants.create(req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled']) + + self.assertRaises(exceptions.Conflict, create_duplicate_tenant) + + def test_delete(self): + self.stub_url('DELETE', ['tenants', self.ADMIN_ID], status_code=204) + self.client.tenants.delete(self.ADMIN_ID) + + def test_get(self): + resp = {'tenant': self.TEST_TENANTS['tenants']['values'][2]} + self.stub_url('GET', ['tenants', self.ADMIN_ID], json=resp) + + t = self.client.tenants.get(self.ADMIN_ID) + self.assertIsInstance(t, tenants.Tenant) + self.assertEqual(t.id, self.ADMIN_ID) + self.assertEqual(t.name, 'admin') + + def test_list(self): + self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list() + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + def test_list_limit(self): + self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list(limit=1) + self.assertQueryStringIs('limit=1') + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + def test_list_marker(self): + self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list(marker=1) + self.assertQueryStringIs('marker=1') + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + def test_list_limit_marker(self): + self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list(limit=1, marker=1) + self.assertQueryStringIs('marker=1&limit=1') + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + def test_update(self): + req_body = { + "tenant": { + "id": self.EXTRAS_ID, + "name": "tenantX", + "description": "I changed you!", + "enabled": False, + "extravalue01": "metadataChanged", + # "extraname": "dontoverwrite!", + }, + } + resp_body = { + "tenant": { + "name": "tenantX", + "enabled": False, + "id": self.EXTRAS_ID, + "description": "I changed you!", + "extravalue01": "metadataChanged", + }, + } + + self.stub_url('POST', ['tenants', self.EXTRAS_ID], json=resp_body) + + tenant = self.client.tenants.update( + req_body['tenant']['id'], + req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled'], + extravalue01=req_body['tenant']['extravalue01'], + name="don't overwrite priors") + self.assertIsInstance(tenant, tenants.Tenant) + self.assertRequestBodyIs(json=req_body) + self.assertEqual(tenant.id, self.EXTRAS_ID) + self.assertEqual(tenant.name, "tenantX") + self.assertEqual(tenant.description, "I changed you!") + self.assertFalse(tenant.enabled) + self.assertEqual(tenant.extravalue01, "metadataChanged") + + def test_update_empty_description(self): + req_body = { + "tenant": { + "id": self.EXTRAS_ID, + "name": "tenantX", + "description": "", + "enabled": False, + }, + } + resp_body = { + "tenant": { + "name": "tenantX", + "enabled": False, + "id": self.EXTRAS_ID, + "description": "", + }, + } + self.stub_url('POST', ['tenants', self.EXTRAS_ID], json=resp_body) + + tenant = self.client.tenants.update(req_body['tenant']['id'], + req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled']) + self.assertIsInstance(tenant, tenants.Tenant) + self.assertRequestBodyIs(json=req_body) + self.assertEqual(tenant.id, self.EXTRAS_ID) + self.assertEqual(tenant.name, "tenantX") + self.assertEqual(tenant.description, "") + self.assertFalse(tenant.enabled) + + def test_add_user(self): + self.stub_url('PUT', + ['tenants', self.EXTRAS_ID, 'users', 'foo', 'roles', + 'OS-KSADM', 'barrr'], + status_code=204) + + self.client.tenants.add_user(self.EXTRAS_ID, 'foo', 'barrr') + + def test_remove_user(self): + self.stub_url('DELETE', ['tenants', self.EXTRAS_ID, 'users', + 'foo', 'roles', 'OS-KSADM', 'barrr'], + status_code=204) + + self.client.tenants.remove_user(self.EXTRAS_ID, 'foo', 'barrr') + + def test_tenant_add_user(self): + self.stub_url('PUT', ['tenants', self.EXTRAS_ID, 'users', + 'foo', 'roles', 'OS-KSADM', 'barrr'], + status_code=204) + + req_body = { + "tenant": { + "id": self.EXTRAS_ID, + "name": "tenantX", + "description": "I changed you!", + "enabled": False, + }, + } + # make tenant object with manager + tenant = self.client.tenants.resource_class(self.client.tenants, + req_body['tenant']) + tenant.add_user('foo', 'barrr') + self.assertIsInstance(tenant, tenants.Tenant) + + def test_tenant_remove_user(self): + self.stub_url('DELETE', ['tenants', self.EXTRAS_ID, 'users', + 'foo', 'roles', 'OS-KSADM', 'barrr'], + status_code=204) + + req_body = { + "tenant": { + "id": self.EXTRAS_ID, + "name": "tenantX", + "description": "I changed you!", + "enabled": False, + }, + } + + # make tenant object with manager + tenant = self.client.tenants.resource_class(self.client.tenants, + req_body['tenant']) + tenant.remove_user('foo', 'barrr') + self.assertIsInstance(tenant, tenants.Tenant) + + def test_tenant_list_users(self): + tenant_id = uuid.uuid4().hex + user_id1 = uuid.uuid4().hex + user_id2 = uuid.uuid4().hex + + tenant_resp = { + 'tenant': { + 'name': uuid.uuid4().hex, + 'enabled': True, + 'id': tenant_id, + 'description': 'test tenant', + } + } + + users_resp = { + 'users': { + 'values': [ + { + 'email': uuid.uuid4().hex, + 'enabled': True, + 'id': user_id1, + 'name': uuid.uuid4().hex, + }, + { + 'email': uuid.uuid4().hex, + 'enabled': True, + 'id': user_id2, + 'name': uuid.uuid4().hex, + }, + ] + } + } + + self.stub_url('GET', ['tenants', tenant_id], json=tenant_resp) + self.stub_url('GET', + ['tenants', tenant_id, 'users'], + json=users_resp) + + tenant = self.client.tenants.get(tenant_id) + user_objs = tenant.list_users() + + for u in user_objs: + self.assertIsInstance(u, users.User) + + self.assertEqual(set([user_id1, user_id2]), + set([u.id for u in user_objs])) + + def test_list_tenants_use_admin_url(self): + self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list() + self.assertEqual(self.TEST_URL + '/tenants', + self.requests_mock.last_request.url) + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + self.assertEqual(len(self.TEST_TENANTS['tenants']['values']), + len(tenant_list)) + + def test_list_tenants_fallback_to_auth_url(self): + new_auth_url = 'http://keystone.test:5000/v2.0' + + token = fixture.V2Token(token_id=self.TEST_TOKEN, + user_name=self.TEST_USER, + user_id=self.TEST_USER_ID) + + self.stub_auth(base_url=new_auth_url, json=token) + self.stub_url('GET', ['tenants'], base_url=new_auth_url, + json=self.TEST_TENANTS) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + c = client.Client(username=self.TEST_USER, + auth_url=new_auth_url, + password=uuid.uuid4().hex) + + self.assertIsNone(c.management_url) + tenant_list = c.tenants.list() + [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list] + + self.assertEqual(len(self.TEST_TENANTS['tenants']['values']), + len(tenant_list)) diff --git a/keystoneclient/tests/unit/v2_0/test_tokens.py b/keystoneclient/tests/unit/v2_0/test_tokens.py new file mode 100644 index 000000000..58df7c219 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_tokens.py @@ -0,0 +1,217 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1 import exceptions +from keystoneauth1 import fixture + +from keystoneclient import access +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import client +from keystoneclient.v2_0 import tokens + + +class TokenTests(utils.ClientTestCase): + + def test_delete(self): + id_ = uuid.uuid4().hex + self.stub_url('DELETE', ['tokens', id_], status_code=204) + self.client.tokens.delete(id_) + + def test_user_password(self): + token_fixture = fixture.V2Token(user_name=self.TEST_USER) + self.stub_auth(json=token_fixture) + + password = uuid.uuid4().hex + token_ref = self.client.tokens.authenticate(username=self.TEST_USER, + password=password) + + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + req_body = { + 'auth': { + 'passwordCredentials': { + 'username': self.TEST_USER, + 'password': password, + } + } + } + + self.assertRequestBodyIs(json=req_body) + + def test_with_token_id(self): + token_fixture = fixture.V2Token() + self.stub_auth(json=token_fixture) + + token_id = uuid.uuid4().hex + token_ref = self.client.tokens.authenticate(token=token_id) + + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + req_body = { + 'auth': { + 'token': { + 'id': token_id, + } + } + } + + self.assertRequestBodyIs(json=req_body) + + def test_without_auth_params(self): + self.assertRaises(ValueError, self.client.tokens.authenticate) + self.assertRaises(ValueError, self.client.tokens.authenticate, + tenant_id=uuid.uuid4().hex) + + def test_with_tenant_id(self): + token_fixture = fixture.V2Token() + token_fixture.set_scope() + self.stub_auth(json=token_fixture) + + token_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + token_ref = self.client.tokens.authenticate(token=token_id, + tenant_id=tenant_id) + + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + tenant_data = {'id': token_fixture.tenant_id, + 'name': token_fixture.tenant_name} + self.assertEqual(tenant_data, token_ref.tenant) + + req_body = { + 'auth': { + 'token': { + 'id': token_id, + }, + 'tenantId': tenant_id + } + } + + self.assertRequestBodyIs(json=req_body) + + def test_with_tenant_name(self): + token_fixture = fixture.V2Token() + token_fixture.set_scope() + self.stub_auth(json=token_fixture) + + token_id = uuid.uuid4().hex + tenant_name = uuid.uuid4().hex + token_ref = self.client.tokens.authenticate(token=token_id, + tenant_name=tenant_name) + + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + tenant_data = {'id': token_fixture.tenant_id, + 'name': token_fixture.tenant_name} + self.assertEqual(tenant_data, token_ref.tenant) + + req_body = { + 'auth': { + 'token': { + 'id': token_id, + }, + 'tenantName': tenant_name + } + } + + self.assertRequestBodyIs(json=req_body) + + def test_authenticate_use_admin_url(self): + token_fixture = fixture.V2Token() + token_fixture.set_scope() + self.stub_auth(json=token_fixture) + + token_ref = self.client.tokens.authenticate(token=uuid.uuid4().hex) + self.assertEqual(self.TEST_URL + '/tokens', + self.requests_mock.last_request.url) + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + def test_authenticate_fallback_to_auth_url(self): + new_auth_url = 'http://keystone.test:5000/v2.0' + + token_fixture = fixture.V2Token() + self.stub_auth(base_url=new_auth_url, json=token_fixture) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + c = client.Client(username=self.TEST_USER, + auth_url=new_auth_url, + password=uuid.uuid4().hex) + + self.assertIsNone(c.management_url) + + token_ref = c.tokens.authenticate(token=uuid.uuid4().hex) + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(token_fixture.token_id, token_ref.id) + self.assertEqual(token_fixture.expires_str, token_ref.expires) + + def test_validate_token(self): + id_ = uuid.uuid4().hex + token_fixture = fixture.V2Token(token_id=id_) + self.stub_url('GET', ['tokens', id_], json=token_fixture) + + token_data = self.client.tokens.get_token_data(id_) + self.assertEqual(token_fixture, token_data) + + token_ref = self.client.tokens.validate(id_) + self.assertIsInstance(token_ref, tokens.Token) + self.assertEqual(id_, token_ref.id) + + def test_validate_token_invalid_token(self): + # If the token is invalid, typically a NotFound is raised. + + id_ = uuid.uuid4().hex + # The server is expected to return 404 if the token is invalid. + self.stub_url('GET', ['tokens', id_], status_code=404) + + self.assertRaises(exceptions.NotFound, + self.client.tokens.get_token_data, id_) + self.assertRaises(exceptions.NotFound, + self.client.tokens.validate, id_) + + def test_validate_token_access_info_with_token_id(self): + # Can validate a token passing a string token ID. + token_id = uuid.uuid4().hex + token_fixture = fixture.V2Token(token_id=token_id) + self.stub_url('GET', ['tokens', token_id], json=token_fixture) + access_info = self.client.tokens.validate_access_info(token_id) + self.assertIsInstance(access_info, access.AccessInfoV2) + self.assertEqual(token_id, access_info.auth_token) + + def test_validate_token_access_info_with_access_info(self): + # Can validate a token passing an access info. + token_id = uuid.uuid4().hex + token_fixture = fixture.V2Token(token_id=token_id) + self.stub_url('GET', ['tokens', token_id], json=token_fixture) + token = access.AccessInfo.factory(body=token_fixture) + access_info = self.client.tokens.validate_access_info(token) + self.assertIsInstance(access_info, access.AccessInfoV2) + self.assertEqual(token_id, access_info.auth_token) + + def test_get_revoked(self): + sample_revoked_response = {'signed': '-----BEGIN CMS-----\nMIIB...'} + self.stub_url('GET', ['tokens', 'revoked'], + json=sample_revoked_response) + resp = self.client.tokens.get_revoked() + self.assertEqual(sample_revoked_response, resp) diff --git a/keystoneclient/tests/unit/v2_0/test_users.py b/keystoneclient/tests/unit/v2_0/test_users.py new file mode 100644 index 000000000..d65073887 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/test_users.py @@ -0,0 +1,301 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v2_0 import utils +from keystoneclient.v2_0 import roles +from keystoneclient.v2_0 import users + + +class UserTests(utils.ClientTestCase): + def setUp(self): + super(UserTests, self).setUp() + self.ADMIN_USER_ID = uuid.uuid4().hex + self.DEMO_USER_ID = uuid.uuid4().hex + self.TEST_USERS = { + "users": { + "values": [ + { + "email": "None", + "enabled": True, + "id": self.ADMIN_USER_ID, + "name": "admin", + }, + { + "email": "None", + "enabled": True, + "id": self.DEMO_USER_ID, + "name": "demo", + }, + ] + } + } + + def test_create(self): + tenant_id = uuid.uuid4().hex + user_id = uuid.uuid4().hex + password = uuid.uuid4().hex + req_body = { + "user": { + "name": "gabriel", + "password": password, + "tenantId": tenant_id, + "email": "test@example.com", + "enabled": True, + } + } + + resp_body = { + "user": { + "name": "gabriel", + "enabled": True, + "tenantId": tenant_id, + "id": user_id, + "password": password, + "email": "test@example.com", + } + } + + self.stub_url('POST', ['users'], json=resp_body) + + user = self.client.users.create(req_body['user']['name'], + req_body['user']['password'], + req_body['user']['email'], + tenant_id=req_body['user']['tenantId'], + enabled=req_body['user']['enabled']) + self.assertIsInstance(user, users.User) + self.assertEqual(user.id, user_id) + self.assertEqual(user.name, "gabriel") + self.assertEqual(user.email, "test@example.com") + self.assertRequestBodyIs(json=req_body) + self.assertNotIn(password, self.logger.output) + + def test_create_user_without_email(self): + tenant_id = uuid.uuid4().hex + req_body = { + "user": { + "name": "gabriel", + "password": "test", + "tenantId": tenant_id, + "enabled": True, + "email": None, + } + } + + user_id = uuid.uuid4().hex + resp_body = { + "user": { + "name": "gabriel", + "enabled": True, + "tenantId": tenant_id, + "id": user_id, + "password": "test", + } + } + + self.stub_url('POST', ['users'], json=resp_body) + + user = self.client.users.create( + req_body['user']['name'], + req_body['user']['password'], + tenant_id=req_body['user']['tenantId'], + enabled=req_body['user']['enabled']) + self.assertIsInstance(user, users.User) + self.assertEqual(user.id, user_id) + self.assertEqual(user.name, "gabriel") + self.assertRequestBodyIs(json=req_body) + + def test_create_user_without_password(self): + user_name = 'test' + user_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + user_enabled = True + req_body = { + 'user': { + 'name': user_name, + 'password': None, + 'tenantId': tenant_id, + 'enabled': user_enabled, + 'email': None, + } + } + resp_body = { + 'user': { + 'name': user_name, + 'enabled': user_enabled, + 'tenantId': tenant_id, + 'id': user_id, + } + } + + self.stub_url('POST', ['users'], json=resp_body) + + user = self.client.users.create(user_name, tenant_id=tenant_id, + enabled=user_enabled) + self.assertIsInstance(user, users.User) + self.assertEqual(user_id, user.id) + self.assertEqual(user_name, user.name) + self.assertRequestBodyIs(json=req_body) + + def test_delete(self): + self.stub_url('DELETE', ['users', self.ADMIN_USER_ID], status_code=204) + self.client.users.delete(self.ADMIN_USER_ID) + + def test_get(self): + self.stub_url('GET', ['users', self.ADMIN_USER_ID], + json={'user': self.TEST_USERS['users']['values'][0]}) + + u = self.client.users.get(self.ADMIN_USER_ID) + self.assertIsInstance(u, users.User) + self.assertEqual(u.id, self.ADMIN_USER_ID) + self.assertEqual(u.name, 'admin') + + def test_list(self): + self.stub_url('GET', ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list() + [self.assertIsInstance(u, users.User) for u in user_list] + + def test_list_limit(self): + self.stub_url('GET', ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(limit=1) + self.assertQueryStringIs('limit=1') + [self.assertIsInstance(u, users.User) for u in user_list] + + def test_list_marker(self): + self.stub_url('GET', ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(marker='foo') + self.assertQueryStringIs('marker=foo') + [self.assertIsInstance(u, users.User) for u in user_list] + + def test_list_limit_marker(self): + self.stub_url('GET', ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(limit=1, marker='foo') + + self.assertQueryStringIs('marker=foo&limit=1') + [self.assertIsInstance(u, users.User) for u in user_list] + + def test_update(self): + req_1 = { + "user": { + "email": "gabriel@example.com", + "name": "gabriel", + } + } + password = uuid.uuid4().hex + req_2 = { + "user": { + "password": password, + } + } + tenant_id = uuid.uuid4().hex + req_3 = { + "user": { + "tenantId": tenant_id, + } + } + req_4 = { + "user": { + "enabled": False, + } + } + + self.stub_url('PUT', ['users', self.DEMO_USER_ID], json=req_1) + self.stub_url('PUT', + ['users', self.DEMO_USER_ID, 'OS-KSADM', 'password'], + json=req_2) + self.stub_url('PUT', + ['users', self.DEMO_USER_ID, 'OS-KSADM', 'tenant'], + json=req_3) + self.stub_url('PUT', + ['users', self.DEMO_USER_ID, 'OS-KSADM', 'enabled'], + json=req_4) + + self.client.users.update(self.DEMO_USER_ID, + name='gabriel', + email='gabriel@example.com') + self.assertRequestBodyIs(json=req_1) + self.client.users.update_password(self.DEMO_USER_ID, password) + self.assertRequestBodyIs(json=req_2) + self.client.users.update_tenant(self.DEMO_USER_ID, tenant_id) + self.assertRequestBodyIs(json=req_3) + self.client.users.update_enabled(self.DEMO_USER_ID, False) + self.assertRequestBodyIs(json=req_4) + self.assertNotIn(password, self.logger.output) + + def test_update_own_password(self): + old_password = uuid.uuid4().hex + new_password = uuid.uuid4().hex + req_body = { + 'user': { + 'password': new_password, + 'original_password': old_password + } + } + resp_body = { + 'access': {} + } + self.stub_url('PATCH', + ['OS-KSCRUD', 'users', self.TEST_USER_ID], + json=resp_body) + + self.client.users.update_own_password(old_password, new_password) + self.assertRequestBodyIs(json=req_body) + self.assertNotIn(old_password, self.logger.output) + self.assertNotIn(new_password, self.logger.output) + + def test_user_role_listing(self): + user_id = uuid.uuid4().hex + role_id1 = uuid.uuid4().hex + role_id2 = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + + user_resp = { + 'user': { + 'id': user_id, + 'email': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + } + } + + roles_resp = { + 'roles': { + 'values': [ + { + 'name': uuid.uuid4().hex, + 'id': role_id1, + }, + { + 'name': uuid.uuid4().hex, + 'id': role_id2, + } + ] + } + } + + self.stub_url('GET', ['users', user_id], json=user_resp) + self.stub_url('GET', + ['tenants', tenant_id, 'users', user_id, 'roles'], + json=roles_resp) + + user = self.client.users.get(user_id) + role_objs = user.list_roles(tenant_id) + + for r in role_objs: + self.assertIsInstance(r, roles.Role) + + self.assertEqual(set([role_id1, role_id2]), + set([r.id for r in role_objs])) diff --git a/keystoneclient/tests/unit/v2_0/utils.py b/keystoneclient/tests/unit/v2_0/utils.py new file mode 100644 index 000000000..6532d71b5 --- /dev/null +++ b/keystoneclient/tests/unit/v2_0/utils.py @@ -0,0 +1,90 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit import utils + + +class UnauthenticatedTestCase(utils.TestCase): + """Class used as base for unauthenticated calls.""" + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0') + + +class TestCase(UnauthenticatedTestCase): + + TEST_ADMIN_IDENTITY_ENDPOINT = "http://127.0.0.1:35357/v2.0" + + TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "adminURL": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8774/v1.0", + "publicURL": "http://cdn.admin-nets.local:8774/v1.0/" + }], + "type": "nova_compat", + "name": "nova_compat" + }, { + "endpoints": [{ + "adminURL": "http://nova/novapi/admin", + "region": "RegionOne", + "internalURL": "http://nova/novapi/internal", + "publicURL": "http://nova/novapi/public" + }], + "type": "compute", + "name": "nova" + }, { + "endpoints": [{ + "adminURL": "http://glance/glanceapi/admin", + "region": "RegionOne", + "internalURL": "http://glance/glanceapi/internal", + "publicURL": "http://glance/glanceapi/public" + }], + "type": "image", + "name": "glance" + }, { + "endpoints": [{ + "adminURL": TEST_ADMIN_IDENTITY_ENDPOINT, + "region": "RegionOne", + "internalURL": "http://127.0.0.1:5000/v2.0", + "publicURL": "http://127.0.0.1:5000/v2.0" + }], + "type": "identity", + "name": "keystone" + }, { + "endpoints": [{ + "adminURL": "http://swift/swiftapi/admin", + "region": "RegionOne", + "internalURL": "http://swift/swiftapi/internal", + "publicURL": "http://swift/swiftapi/public" + }], + "type": "object-store", + "name": "swift" + }] + + def stub_auth(self, **kwargs): + self.stub_url('POST', ['tokens'], **kwargs) + + +class ClientTestCase(utils.ClientTestCaseMixin, TestCase): + + scenarios = [ + ('original', + {'client_fixture_class': client_fixtures.OriginalV2}), + ('ksc-session', + {'client_fixture_class': client_fixtures.KscSessionV2}), + ('ksa-session', + {'client_fixture_class': client_fixtures.KsaSessionV2}), + ] diff --git a/keystoneclient/tests/unit/v3/__init__.py b/keystoneclient/tests/unit/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/keystoneclient/tests/unit/v3/client_fixtures.py b/keystoneclient/tests/unit/v3/client_fixtures.py new file mode 100644 index 000000000..e22e7da1c --- /dev/null +++ b/keystoneclient/tests/unit/v3/client_fixtures.py @@ -0,0 +1,157 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import uuid + +from keystoneauth1 import fixture + + +def unscoped_token(**kwargs): + return fixture.V3Token(**kwargs) + + +def domain_scoped_token(**kwargs): + kwargs.setdefault('audit_chain_id', uuid.uuid4().hex) + f = fixture.V3Token(**kwargs) + if not f.domain_id: + f.set_domain_scope() + + f.add_role(name='admin') + f.add_role(name='member') + region = 'RegionOne' + + s = f.add_service('volume') + s.add_standard_endpoints(public='http://public.com:8776/v1/None', + internal='http://internal.com:8776/v1/None', + admin='http://admin.com:8776/v1/None', + region=region) + + s = f.add_service('image') + s.add_standard_endpoints(public='http://public.com:9292/v1', + internal='http://internal:9292/v1', + admin='http://admin:9292/v1', + region=region) + + s = f.add_service('compute') + s.add_standard_endpoints(public='http://public.com:8774/v1.1/None', + internal='http://internal:8774/v1.1/None', + admin='http://admin:8774/v1.1/None', + region=region) + + s = f.add_service('ec2') + s.add_standard_endpoints(public='http://public.com:8773/services/Cloud', + internal='http://internal:8773/services/Cloud', + admin='http://admin:8773/services/Admin', + region=region) + + s = f.add_service('identity') + s.add_standard_endpoints(public='http://public.com:5000/v3', + internal='http://internal:5000/v3', + admin='http://admin:35357/v3', + region=region) + + return f + + +def project_scoped_token(**kwargs): + kwargs.setdefault('audit_chain_id', uuid.uuid4().hex) + f = fixture.V3Token(**kwargs) + + if not f.project_id: + f.set_project_scope() + + f.add_role(name='admin') + f.add_role(name='member') + + region = 'RegionOne' + tenant = '225da22d3ce34b15877ea70b2a575f58' + + s = f.add_service('volume') + s.add_standard_endpoints(public='http://public.com:8776/v1/%s' % tenant, + internal='http://internal:8776/v1/%s' % tenant, + admin='http://admin:8776/v1/%s' % tenant, + region=region) + + s = f.add_service('image') + s.add_standard_endpoints(public='http://public.com:9292/v1', + internal='http://internal:9292/v1', + admin='http://admin:9292/v1', + region=region) + + s = f.add_service('compute') + s.add_standard_endpoints(public='http://public.com:8774/v2/%s' % tenant, + internal='http://internal:8774/v2/%s' % tenant, + admin='http://admin:8774/v2/%s' % tenant, + region=region) + + s = f.add_service('ec2') + s.add_standard_endpoints(public='http://public.com:8773/services/Cloud', + internal='http://internal:8773/services/Cloud', + admin='http://admin:8773/services/Admin', + region=region) + + s = f.add_service('identity') + s.add_standard_endpoints(public='http://public.com:5000/v3', + internal='http://internal:5000/v3', + admin='http://admin:35357/v3', + region=region) + + return f + + +AUTH_SUBJECT_TOKEN = uuid.uuid4().hex + +AUTH_RESPONSE_HEADERS = { + 'X-Subject-Token': AUTH_SUBJECT_TOKEN, +} + + +def auth_response_body(): + f = fixture.V3Token(audit_chain_id=uuid.uuid4().hex) + f.set_project_scope() + + f.add_role(name='admin') + f.add_role(name='member') + + s = f.add_service('compute', name='nova') + s.add_standard_endpoints( + public='https://compute.north.host/novapi/public', + internal='https://compute.north.host/novapi/internal', + admin='https://compute.north.host/novapi/admin', + region='North') + + s = f.add_service('object-store', name='swift') + s.add_standard_endpoints( + public='http://swift.north.host/swiftapi/public', + internal='http://swift.north.host/swiftapi/internal', + admin='http://swift.north.host/swiftapi/admin', + region='South') + + s = f.add_service('image', name='glance') + s.add_standard_endpoints( + public='http://glance.north.host/glanceapi/public', + internal='http://glance.north.host/glanceapi/internal', + admin='http://glance.north.host/glanceapi/admin', + region='North') + + s.add_standard_endpoints( + public='http://glance.south.host/glanceapi/public', + internal='http://glance.south.host/glanceapi/internal', + admin='http://glance.south.host/glanceapi/admin', + region='South') + + return f + + +def trust_token(): + f = fixture.V3Token(audit_chain_id=uuid.uuid4().hex) + f.set_trust_scope() + return f diff --git a/keystoneclient/tests/unit/v3/examples/xml/ADFS_RequestSecurityTokenResponse.xml b/keystoneclient/tests/unit/v3/examples/xml/ADFS_RequestSecurityTokenResponse.xml new file mode 100644 index 000000000..487bcac59 --- /dev/null +++ b/keystoneclient/tests/unit/v3/examples/xml/ADFS_RequestSecurityTokenResponse.xml @@ -0,0 +1,132 @@ + + + http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal + urn:uuid:487c064b-b7c6-4654-b4d4-715f9961170e + + + 2014-08-05T18:36:14.235Z + 2014-08-05T18:41:14.235Z + + + + + + + + 2014-08-05T18:36:14.063Z + 2014-08-05T19:36:14.063Z + + + + https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS + + + + + + + https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS + + + + + marek.denis@cern.ch + + urn:oasis:names:tc:SAML:1.0:cm:bearer + + + + marek.denis@cern.ch + + + marek.denis@cern.ch + + + madenis + + + CERN Users + + + Domain Users + occupants-bldg-31 + CERN-Direct-Employees + ca-dev-allowed + cernts-cerntstest-users + staf-fell-pjas-at-cern + ELG-CERN + student-club-new-members + pawel-dynamic-test-82 + + + Marek Kamil Denis + + + +5555555 + + + 31S-013 + + + Marek Kamil + + + Denis + + + CERN Registered + + + CERN + + + Normal + + + + + marek.denis@cern.ch + + urn:oasis:names:tc:SAML:1.0:cm:bearer + + + + + + + + + + + + + + EaZ/2d0KAY5un9akV3++Npyk6hBc8JuTYs2S3lSxUeQ= + + + CxYiYvNsbedhHdmDbb9YQCBy6Ppus3bNJdw2g2HLq0VU2yRhv23mUW05I89Hs4yG4OcCo0uOZ3zaeNFbSNXMW+Mr996tAXtujKjgyrCXNJAToE+gwltvGxwY1EluSbe3IzoSM3Ao87mKhxGOSzlDhuN7dQ9Rv6l/J4gUjbOO5SIX4pdZ6mVF7cHEfe9x+H8Lg15YjnElQUEaPi+NSW5jYTdtIpsB4ORxJvALuSt6+4doDYc9wuwBiWkEdnBHAQBINoKpAV2oy0/C85SBX3IdRhxUznmL5yEUmf8JvPccXecMPqJow0L43mnCdu74xPwU0as3MNfYQ10kLvHXHfIExg== + + + MIIIEjCCBfqgAwIBAgIKLYgjvQAAAAAAMDANBgkqhkiG9w0BAQsFADBRMRIwEAYKCZImiZPyLGQBGRYCY2gxFDASBgoJkiaJk/IsZAEZFgRjZXJuMSUwIwYDVQQDExxDRVJOIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMTEwODA4Mzg1NVoXDTIzMDcyOTA5MTkzOFowVjESMBAGCgmSJomT8ixkARkWAmNoMRQwEgYKCZImiZPyLGQBGRYEY2VybjESMBAGA1UECxMJY29tcHV0ZXJzMRYwFAYDVQQDEw1sb2dpbi5jZXJuLmNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp6t1C0SGlLddL2M+ltffGioTnDT3eztOxlA9bAGuvB8/Rjym8en6+ET9boM02CyoR5Vpn8iElXVWccAExPIQEq70D6LPe86vb+tYhuKPeLfuICN9Z0SMQ4f+57vk61Co1/uw/8kPvXlyd+Ai8Dsn/G0hpH67bBI9VOQKfpJqclcSJuSlUB5PJffvMUpr29B0eRx8LKFnIHbDILSu6nVbFLcadtWIjbYvoKorXg3J6urtkz+zEDeYMTvA6ZGOFf/Xy5eGtroSq9csSC976tx+umKEPhXBA9AcpiCV9Cj5axN03Aaa+iTE36jpnjcd9d02dy5Q9jE2nUN6KXnB6qF6eQIDAQABo4ID5TCCA+EwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIg73QCYLtjQ2G7Ysrgd71N4WA0GIehd2yb4Wu9TkCAWQCARkwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIFoDBoBgNVHSAEYTBfMF0GCisGAQQBYAoEAQEwTzBNBggrBgEFBQcCARZBaHR0cDovL2NhLWRvY3MuY2Vybi5jaC9jYS1kb2NzL2NwLWNwcy9jZXJuLXRydXN0ZWQtY2EyLWNwLWNwcy5wZGYwJwYJKwYBBAGCNxUKBBowGDAKBggrBgEFBQcDAjAKBggrBgEFBQcDATAdBgNVHQ4EFgQUqtJcwUXasyM6sRaO5nCMFoFDenMwGAYDVR0RBBEwD4INbG9naW4uY2Vybi5jaDAfBgNVHSMEGDAWgBQdkBnqyM7MPI0UsUzZ7BTiYUADYTCCASoGA1UdHwSCASEwggEdMIIBGaCCARWgggERhkdodHRwOi8vY2FmaWxlcy5jZXJuLmNoL2NhZmlsZXMvY3JsL0NFUk4lMjBDZXJ0aWZpY2F0aW9uJTIwQXV0aG9yaXR5LmNybIaBxWxkYXA6Ly8vQ049Q0VSTiUyMENlcnRpZmljYXRpb24lMjBBdXRob3JpdHksQ049Q0VSTlBLSTA3LENOPUNEUCxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWNlcm4sREM9Y2g/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIIBVAYIKwYBBQUHAQEEggFGMIIBQjBcBggrBgEFBQcwAoZQaHR0cDovL2NhZmlsZXMuY2Vybi5jaC9jYWZpbGVzL2NlcnRpZmljYXRlcy9DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eS5jcnQwgbsGCCsGAQUFBzAChoGubGRhcDovLy9DTj1DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1jZXJuLERDPWNoP2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jZXJuLmNoL29jc3AwDQYJKoZIhvcNAQELBQADggIBAGKZ3bknTCfNuh4TMaL3PuvBFjU8LQ5NKY9GLZvY2ibYMRk5Is6eWRgyUsy1UJRQdaQQPnnysqrGq8VRw/NIFotBBsA978/+jj7v4e5Kr4o8HvwAQNLBxNmF6XkDytpLL701FcNEGRqIsoIhNzihi2VBADLC9HxljEyPT52IR767TMk/+xTOqClceq3sq6WRD4m+xaWRUJyOhn+Pqr+wbhXIw4wzHC6X0hcLj8P9Povtm6VmKkN9JPuymMo/0+zSrUt2+TYfmbbEKYJSP0+sceQ76IKxxmSdKAr1qDNE8v+c3DvPM2PKmfivwaV2l44FdP8ulzqTgphkYcN1daa9Oc+qJeyu/eL7xWzk6Zq5R+jVrMlM0p1y2XczI7Hoc96TMOcbVnwgMcVqRM9p57VItn6XubYPR0C33i1yUZjkWbIfqEjq6Vev6lVgngOyzu+hqC/8SDyORA3dlF9aZOD13kPZdF/JRphHREQtaRydAiYRlE/WHTvOcY52jujDftUR6oY0eWaWkwSHbX+kDFx8IlR8UtQCUgkGHBGwnOYLIGu7SRDGSfOBOiVhxKoHWVk/pL6eKY2SkmyOmmgO4JnQGg95qeAOMG/EQZt/2x8GAavUqGvYy9dPFwFf08678hQqkjNSuex7UD0ku8OP1QKvpP44l6vZhFc6A5XqjdU9lus1 + + + + + + + + _c9e77bc4-a81b-4da7-88c2-72a6ba376d3f + + + + + _c9e77bc4-a81b-4da7-88c2-72a6ba376d3f + + + urn:oasis:names:tc:SAML:1.0:assertion + http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue + http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer + + + + \ No newline at end of file diff --git a/keystoneclient/tests/unit/v3/examples/xml/ADFS_fault.xml b/keystoneclient/tests/unit/v3/examples/xml/ADFS_fault.xml new file mode 100644 index 000000000..913252e7f --- /dev/null +++ b/keystoneclient/tests/unit/v3/examples/xml/ADFS_fault.xml @@ -0,0 +1,19 @@ + + + http://www.w3.org/2005/08/addressing/soap/fault + urn:uuid:89c47849-2622-4cdc-bb06-1d46c89ed12d + + + + + s:Sender + + a:FailedAuthentication + + + + At least one security token in the message could not be validated. + + + + \ No newline at end of file diff --git a/keystoneclient/tests/unit/v3/saml2_fixtures.py b/keystoneclient/tests/unit/v3/saml2_fixtures.py new file mode 100644 index 000000000..17c1395a8 --- /dev/null +++ b/keystoneclient/tests/unit/v3/saml2_fixtures.py @@ -0,0 +1,279 @@ +# 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. + +SP_SOAP_RESPONSE = b""" + + + + +https://openstack4.local/shibboleth + + + + + +ss:mem:6f1f20fafbb38433467e9d477df67615 + + + https://openstack4.local/shibboleth + + + + +""" + + +SAML2_ASSERTION = b""" + + + + + x= + + + + + +https://idp.testshib.org/idp/shibboleth + + + + + + + + + + + + + + +VALUE== + + +VALUE= + + +""" + +UNSCOPED_TOKEN_HEADER = 'UNSCOPED_TOKEN' + +UNSCOPED_TOKEN = { + "token": { + "issued_at": "2014-06-09T09:48:59.643406Z", + "extras": {}, + "methods": ["saml2"], + "expires_at": "2014-06-09T10:48:59.643375Z", + "user": { + "OS-FEDERATION": { + "identity_provider": { + "id": "testshib" + }, + "protocol": { + "id": "saml2" + }, + "groups": [ + {"id": "1764fa5cf69a49a4918131de5ce4af9a"} + ] + }, + "id": "testhib%20user", + "name": "testhib user" + } + } +} + +PROJECTS = { + "projects": [ + { + "domain_id": "37ef61", + "enabled": 'true', + "id": "12d706", + "links": { + "self": "http://identity:35357/v3/projects/12d706" + }, + "name": "a project name" + }, + { + "domain_id": "37ef61", + "enabled": 'true', + "id": "9ca0eb", + "links": { + "self": "http://identity:35357/v3/projects/9ca0eb" + }, + "name": "another project" + } + ], + "links": { + "self": "http://identity:35357/v3/auth/projects", + "previous": 'null', + "next": 'null' + } +} + +DOMAINS = { + "domains": [ + { + "description": "desc of domain", + "enabled": 'true', + "id": "37ef61", + "links": { + "self": "http://identity:35357/v3/domains/37ef61" + }, + "name": "my domain" + } + ], + "links": { + "self": "http://identity:35357/v3/auth/domains", + "previous": 'null', + "next": 'null' + } +} + +SAML_ENCODING = "" + +TOKEN_SAML_RESPONSE = """ + + + http://keystone.idp/v3/OS-FEDERATION/saml2/idp + + + + + + + http://keystone.idp/v3/OS-FEDERATION/saml2/idp + + + + + + + + + + + + 0KH2CxdkfzU+6eiRhTC+mbObUKI= + + + + + m2jh5gDvX/1k+4uKtbb08CHp2b9UWsLw + + + + ... + + + + + admin + + + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + http://keystone.idp/v3/OS-FEDERATION/saml2/idp + + + + + + admin + + + admin + + + admin + + + + +""" + +TOKEN_BASED_SAML = ''.join([SAML_ENCODING, TOKEN_SAML_RESPONSE]) + +ECP_ENVELOPE = """ + + + + ss:mem:1ddfe8b0f58341a5a840d2e8717b0737 + + + + {0} + + +""".format(TOKEN_SAML_RESPONSE) + +TOKEN_BASED_ECP = ''.join([SAML_ENCODING, ECP_ENVELOPE]) diff --git a/keystoneclient/tests/unit/v3/test_access.py b/keystoneclient/tests/unit/v3/test_access.py new file mode 100644 index 000000000..26ca8f15e --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_access.py @@ -0,0 +1,211 @@ +# 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 fixture +from oslo_utils import timeutils + +from keystoneclient import access +from keystoneclient.tests.unit import utils as test_utils +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import utils + + +TOKEN_RESPONSE = test_utils.test_response( + headers=client_fixtures.AUTH_RESPONSE_HEADERS +) +UNSCOPED_TOKEN = client_fixtures.unscoped_token() +DOMAIN_SCOPED_TOKEN = client_fixtures.domain_scoped_token() +PROJECT_SCOPED_TOKEN = client_fixtures.project_scoped_token() + + +class AccessInfoTest(utils.TestCase): + def test_building_unscoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=UNSCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('methods', auth_ref) + self.assertNotIn('catalog', auth_ref) + + self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, + auth_ref.auth_token) + self.assertEqual(UNSCOPED_TOKEN.user_name, auth_ref.username) + self.assertEqual(UNSCOPED_TOKEN.user_id, auth_ref.user_id) + + self.assertEqual(auth_ref.role_ids, []) + self.assertEqual(auth_ref.role_names, []) + + self.assertIsNone(auth_ref.project_name) + self.assertIsNone(auth_ref.project_id) + + with self.deprecations.expect_deprecations_here(): + self.assertIsNone(auth_ref.auth_url) + with self.deprecations.expect_deprecations_here(): + self.assertIsNone(auth_ref.management_url) + + self.assertFalse(auth_ref.domain_scoped) + self.assertFalse(auth_ref.project_scoped) + + self.assertEqual(UNSCOPED_TOKEN.user_domain_id, + auth_ref.user_domain_id) + self.assertEqual(UNSCOPED_TOKEN.user_domain_name, + auth_ref.user_domain_name) + + self.assertIsNone(auth_ref.project_domain_id) + self.assertIsNone(auth_ref.project_domain_name) + + self.assertEqual(auth_ref.expires, timeutils.parse_isotime( + UNSCOPED_TOKEN['token']['expires_at'])) + self.assertEqual(auth_ref.issued, timeutils.parse_isotime( + UNSCOPED_TOKEN['token']['issued_at'])) + + self.assertEqual(auth_ref.expires, UNSCOPED_TOKEN.expires) + self.assertEqual(auth_ref.issued, UNSCOPED_TOKEN.issued) + + self.assertEqual(auth_ref.audit_id, UNSCOPED_TOKEN.audit_id) + self.assertIsNone(auth_ref.audit_chain_id) + self.assertIsNone(UNSCOPED_TOKEN.audit_chain_id) + + def test_will_expire_soon(self): + expires = timeutils.utcnow() + datetime.timedelta(minutes=5) + UNSCOPED_TOKEN['token']['expires_at'] = expires.isoformat() + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=UNSCOPED_TOKEN) + self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) + self.assertTrue(auth_ref.will_expire_soon(stale_duration=301)) + self.assertFalse(auth_ref.will_expire_soon()) + + def test_building_domain_scoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=DOMAIN_SCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('methods', auth_ref) + self.assertIn('catalog', auth_ref) + self.assertTrue(auth_ref['catalog']) + + self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, + auth_ref.auth_token) + self.assertEqual(DOMAIN_SCOPED_TOKEN.user_name, auth_ref.username) + self.assertEqual(DOMAIN_SCOPED_TOKEN.user_id, auth_ref.user_id) + + self.assertEqual(DOMAIN_SCOPED_TOKEN.role_ids, auth_ref.role_ids) + self.assertEqual(DOMAIN_SCOPED_TOKEN.role_names, auth_ref.role_names) + + self.assertEqual(DOMAIN_SCOPED_TOKEN.domain_name, auth_ref.domain_name) + self.assertEqual(DOMAIN_SCOPED_TOKEN.domain_id, auth_ref.domain_id) + + self.assertIsNone(auth_ref.project_name) + self.assertIsNone(auth_ref.project_id) + + self.assertEqual(DOMAIN_SCOPED_TOKEN.user_domain_id, + auth_ref.user_domain_id) + self.assertEqual(DOMAIN_SCOPED_TOKEN.user_domain_name, + auth_ref.user_domain_name) + + self.assertIsNone(auth_ref.project_domain_id) + self.assertIsNone(auth_ref.project_domain_name) + + self.assertTrue(auth_ref.domain_scoped) + self.assertFalse(auth_ref.project_scoped) + + self.assertEqual(DOMAIN_SCOPED_TOKEN.audit_id, auth_ref.audit_id) + self.assertEqual(DOMAIN_SCOPED_TOKEN.audit_chain_id, + auth_ref.audit_chain_id) + + def test_building_project_scoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=PROJECT_SCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('methods', auth_ref) + self.assertIn('catalog', auth_ref) + self.assertTrue(auth_ref['catalog']) + + self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, + auth_ref.auth_token) + self.assertEqual(PROJECT_SCOPED_TOKEN.user_name, auth_ref.username) + self.assertEqual(PROJECT_SCOPED_TOKEN.user_id, auth_ref.user_id) + + self.assertEqual(PROJECT_SCOPED_TOKEN.role_ids, auth_ref.role_ids) + self.assertEqual(PROJECT_SCOPED_TOKEN.role_names, auth_ref.role_names) + + self.assertIsNone(auth_ref.domain_name) + self.assertIsNone(auth_ref.domain_id) + + self.assertEqual(PROJECT_SCOPED_TOKEN.project_name, + auth_ref.project_name) + self.assertEqual(PROJECT_SCOPED_TOKEN.project_id, auth_ref.project_id) + + self.assertEqual(auth_ref.tenant_name, auth_ref.project_name) + self.assertEqual(auth_ref.tenant_id, auth_ref.project_id) + + with self.deprecations.expect_deprecations_here(): + self.assertEqual(auth_ref.auth_url, + ('http://public.com:5000/v3',)) + with self.deprecations.expect_deprecations_here(): + self.assertEqual(auth_ref.management_url, + ('http://admin:35357/v3',)) + + self.assertEqual(PROJECT_SCOPED_TOKEN.project_domain_id, + auth_ref.project_domain_id) + self.assertEqual(PROJECT_SCOPED_TOKEN.project_domain_name, + auth_ref.project_domain_name) + + self.assertEqual(PROJECT_SCOPED_TOKEN.user_domain_id, + auth_ref.user_domain_id) + self.assertEqual(PROJECT_SCOPED_TOKEN.user_domain_name, + auth_ref.user_domain_name) + + self.assertFalse(auth_ref.domain_scoped) + self.assertTrue(auth_ref.project_scoped) + + self.assertEqual(PROJECT_SCOPED_TOKEN.audit_id, auth_ref.audit_id) + self.assertEqual(PROJECT_SCOPED_TOKEN.audit_chain_id, + auth_ref.audit_chain_id) + + def test_oauth_access(self): + consumer_id = uuid.uuid4().hex + access_token_id = uuid.uuid4().hex + + token = fixture.V3Token() + token.set_project_scope() + token.set_oauth(access_token_id=access_token_id, + consumer_id=consumer_id) + + auth_ref = access.AccessInfo.factory(body=token) + + self.assertEqual(consumer_id, auth_ref.oauth_consumer_id) + self.assertEqual(access_token_id, auth_ref.oauth_access_token_id) + + self.assertEqual(consumer_id, auth_ref['OS-OAUTH1']['consumer_id']) + self.assertEqual(access_token_id, + auth_ref['OS-OAUTH1']['access_token_id']) + + def test_override_auth_token(self): + token = fixture.V3Token() + token.set_project_scope() + + new_auth_token = uuid.uuid4().hex + auth_ref = access.AccessInfo.factory(body=token, + auth_token=new_auth_token) + self.assertEqual(new_auth_token, auth_ref.auth_token) + + def test_federated_property_standard_token(self): + """Check if is_federated property returns expected value.""" + token = fixture.V3Token() + token.set_project_scope() + auth_ref = access.AccessInfo.factory(body=token) + self.assertFalse(auth_ref.is_federated) diff --git a/keystoneclient/tests/unit/v3/test_access_rules.py b/keystoneclient/tests/unit/v3/test_access_rules.py new file mode 100644 index 000000000..d3e22f8dd --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_access_rules.py @@ -0,0 +1,41 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uuid + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import access_rules + + +class AccessRuleTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(AccessRuleTests, self).setUp() + self.key = 'access_rule' + self.collection_key = 'access_rules' + self.model = access_rules.AccessRule + self.manager = self.client.access_rules + self.path_prefix = 'users/%s' % self.TEST_USER_ID + + def new_ref(self, **kwargs): + kwargs = super(AccessRuleTests, self).new_ref(**kwargs) + kwargs.setdefault('path', uuid.uuid4().hex) + kwargs.setdefault('method', uuid.uuid4().hex) + kwargs.setdefault('service', uuid.uuid4().hex) + return kwargs + + def test_update(self): + self.assertRaises(exceptions.MethodNotImplemented, self.manager.update) + + def test_create(self): + self.assertRaises(exceptions.MethodNotImplemented, self.manager.create) diff --git a/keystoneclient/tests/unit/v3/test_application_credentials.py b/keystoneclient/tests/unit/v3/test_application_credentials.py new file mode 100644 index 000000000..6e4bba3e6 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_application_credentials.py @@ -0,0 +1,137 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uuid + +from oslo_utils import timeutils + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import application_credentials + + +class ApplicationCredentialTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(ApplicationCredentialTests, self).setUp() + self.key = 'application_credential' + self.collection_key = 'application_credentials' + self.model = application_credentials.ApplicationCredential + self.manager = self.client.application_credentials + self.path_prefix = 'users/%s' % self.TEST_USER_ID + + def new_ref(self, **kwargs): + kwargs = super(ApplicationCredentialTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('unrestricted', False) + return kwargs + + def test_create_with_roles(self): + ref = self.new_ref(user=uuid.uuid4().hex) + ref['roles'] = [{'name': 'atestrole'}] + req_ref = ref.copy() + req_ref.pop('id') + user = req_ref.pop('user') + + self.stub_entity('POST', + ['users', user, self.collection_key], + status_code=201, entity=req_ref) + + super(ApplicationCredentialTests, self).test_create(ref=ref, + req_ref=req_ref) + + def test_create_with_role_id_and_names(self): + ref = self.new_ref(user=uuid.uuid4().hex) + ref['roles'] = [{'name': 'atestrole', 'domain': 'nondefault'}, + uuid.uuid4().hex] + req_ref = ref.copy() + req_ref.pop('id') + user = req_ref.pop('user') + + req_ref['roles'] = [{'name': 'atestrole', 'domain': 'nondefault'}, + {'id': ref['roles'][1]}] + self.stub_entity('POST', + ['users', user, self.collection_key], + status_code=201, entity=req_ref) + + super(ApplicationCredentialTests, self).test_create(ref=ref, + req_ref=req_ref) + + def test_create_expires(self): + ref = self.new_ref(user=uuid.uuid4().hex) + ref['expires_at'] = timeutils.parse_isotime( + '2013-03-04T12:00:01.000000Z') + req_ref = ref.copy() + req_ref.pop('id') + user = req_ref.pop('user') + + req_ref['expires_at'] = '2013-03-04T12:00:01.000000Z' + + self.stub_entity('POST', + ['users', user, self.collection_key], + status_code=201, entity=req_ref) + + super(ApplicationCredentialTests, self).test_create(ref=ref, + req_ref=req_ref) + + def test_create_unrestricted(self): + ref = self.new_ref(user=uuid.uuid4().hex) + ref['unrestricted'] = True + req_ref = ref.copy() + req_ref.pop('id') + user = req_ref.pop('user') + + self.stub_entity('POST', + ['users', user, self.collection_key], + status_code=201, entity=req_ref) + + super(ApplicationCredentialTests, self).test_create(ref=ref, + req_ref=req_ref) + + def test_create_with_access_rules(self): + ref = self.new_ref(user=uuid.uuid4().hex) + access_rules = [ + { + 'method': 'GET', + 'path': '/v3/projects', + 'service': 'identity' + } + ] + ref['access_rules'] = access_rules + req_ref = ref.copy() + req_ref.pop('id') + user = req_ref.pop('user') + + self.stub_entity('POST', + ['users', user, self.collection_key], + status_code=201, entity=req_ref) + + super(ApplicationCredentialTests, self).test_create(ref=ref, + req_ref=req_ref) + + def test_get(self): + ref = self.new_ref(user=uuid.uuid4().hex) + + self.stub_entity( + 'GET', ['users', ref['user'], self.collection_key, ref['id']], + entity=ref) + returned = self.manager.get(ref['id'], ref['user']) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + def test_update(self): + self.assertRaises(exceptions.MethodNotImplemented, self.manager.update) diff --git a/keystoneclient/tests/unit/v3/test_auth.py b/keystoneclient/tests/unit/v3/test_auth.py new file mode 100644 index 000000000..d3c44adc7 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_auth.py @@ -0,0 +1,372 @@ +# 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 oslo_serialization import jsonutils +from testtools import testcase + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import client + + +class AuthenticateAgainstKeystoneTests(utils.TestCase): + def setUp(self): + super(AuthenticateAgainstKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "token": { + "methods": [ + "token", + "password" + ], + + "expires_at": "2999-01-01T00:00:10.000123Z", + "project": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_TENANT_ID, + "name": self.TEST_TENANT_NAME + }, + "user": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_USER, + "name": self.TEST_USER + }, + "issued_at": "2013-05-29T16:55:21.468960Z", + "catalog": self.TEST_SERVICE_CATALOG + }, + } + self.TEST_REQUEST_BODY = { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": { + "name": self.TEST_DOMAIN_NAME + }, + "name": self.TEST_USER, + "password": self.TEST_TOKEN + } + } + }, + "scope": { + "project": { + "id": self.TEST_TENANT_ID + }, + } + } + } + self.TEST_REQUEST_HEADERS = { + 'Content-Type': 'application/json', + 'User-Agent': 'python-keystoneclient' + } + self.TEST_RESPONSE_HEADERS = { + 'X-Subject-Token': self.TEST_TOKEN + } + + def test_authenticate_success(self): + TEST_TOKEN = "abcdef" + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password']['user']['domain'] + del ident['password']['user']['name'] + ident['password']['user']['id'] = self.TEST_USER + + self.stub_auth(json=self.TEST_RESPONSE_DICT, subject_token=TEST_TOKEN) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client(user_id=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, TEST_TOKEN) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_failure(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + ident['password']['user']['password'] = 'bad_key' + error = {"unauthorized": {"message": "Unauthorized", + "code": "401"}} + + self.stub_auth(status_code=401, json=error) + + with testcase.ExpectedException(exceptions.Unauthorized): + with self.deprecations.expect_deprecations_here(): + client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password="bad_key", + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_auth_redirect(self): + headers = {'Location': self.TEST_ADMIN_URL + '/auth/tokens'} + self.stub_auth(status_code=305, text='Use proxy', headers=headers) + + self.stub_auth(json=self.TEST_RESPONSE_DICT, + base_url=self.TEST_ADMIN_URL) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + + def test_authenticate_success_domain_username_password_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + + def test_authenticate_success_userid_password_domain_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password']['user']['domain'] + del ident['password']['user']['name'] + ident['password']['user']['id'] = self.TEST_USER + + scope = self.TEST_REQUEST_BODY['auth']['scope'] + del scope['project'] + scope['domain'] = {} + scope['domain']['id'] = self.TEST_DOMAIN_ID + + token = self.TEST_RESPONSE_DICT['token'] + del token['project'] + token['domain'] = {} + token['domain']['id'] = self.TEST_DOMAIN_ID + token['domain']['name'] = self.TEST_DOMAIN_NAME + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client(user_id=self.TEST_USER, + password=self.TEST_TOKEN, + domain_id=self.TEST_DOMAIN_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_domain_id, + self.TEST_DOMAIN_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_userid_password_project_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password']['user']['domain'] + del ident['password']['user']['name'] + ident['password']['user']['id'] = self.TEST_USER + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client(user_id=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_tenant_id, + self.TEST_TENANT_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_password_unscoped(self): + del self.TEST_RESPONSE_DICT['token']['catalog'] + del self.TEST_REQUEST_BODY['auth']['scope'] + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertNotIn('catalog', cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_auth_url_token_authentication(self): + fake_token = 'fake_token' + fake_url = '/fake-url' + fake_resp = {'result': True} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [fake_url], json=fake_resp, + base_url=self.TEST_PUBLIC_IDENTITY_ENDPOINT) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = client.Client(auth_url=self.TEST_URL, + token=fake_token) + body = jsonutils.loads(self.requests_mock.last_request.body) + self.assertEqual(body['auth']['identity']['token']['id'], fake_token) + + resp, body = cl._adapter.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests_mock.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) + + def test_authenticate_success_token_domain_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password'] + ident['methods'] = ['token'] + ident['token'] = {} + ident['token']['id'] = self.TEST_TOKEN + + scope = self.TEST_REQUEST_BODY['auth']['scope'] + del scope['project'] + scope['domain'] = {} + scope['domain']['id'] = self.TEST_DOMAIN_ID + + token = self.TEST_RESPONSE_DICT['token'] + del token['project'] + token['domain'] = {} + token['domain']['id'] = self.TEST_DOMAIN_ID + token['domain']['name'] = self.TEST_DOMAIN_NAME + + self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client(token=self.TEST_TOKEN, + domain_id=self.TEST_DOMAIN_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_domain_id, + self.TEST_DOMAIN_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_token_project_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password'] + ident['methods'] = ['token'] + ident['token'] = {} + ident['token']['id'] = self.TEST_TOKEN + self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client(token=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_tenant_id, + self.TEST_TENANT_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_authenticate_success_token_unscoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password'] + ident['methods'] = ['token'] + ident['token'] = {} + ident['token']['id'] = self.TEST_TOKEN + del self.TEST_REQUEST_BODY['auth']['scope'] + del self.TEST_RESPONSE_DICT['token']['catalog'] + self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client(token=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertNotIn('catalog', cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + def test_allow_override_of_auth_token(self): + fake_url = '/fake-url' + fake_token = 'fake_token' + fake_resp = {'result': True} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + self.stub_url('GET', [fake_url], json=fake_resp, + base_url=self.TEST_PUBLIC_IDENTITY_ENDPOINT) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = client.Client(username='exampleuser', + password='password', + project_name='exampleproject', + auth_url=self.TEST_URL) + + self.assertEqual(cl.auth_token, self.TEST_TOKEN) + + # the token returned from the authentication will be used + resp, body = cl._adapter.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests_mock.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) + + # then override that token and the new token shall be used + cl.auth_token = fake_token + + resp, body = cl._adapter.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests_mock.last_request.headers.get('X-Auth-Token') + self.assertEqual(fake_token, token) + + # if we clear that overridden token then we fall back to the original + del cl.auth_token + + resp, body = cl._adapter.get(fake_url) + self.assertEqual(fake_resp, body) + + token = self.requests_mock.last_request.headers.get('X-Auth-Token') + self.assertEqual(self.TEST_TOKEN, token) diff --git a/keystoneclient/tests/unit/v3/test_auth_manager.py b/keystoneclient/tests/unit/v3/test_auth_manager.py new file mode 100644 index 000000000..dec8b0c05 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_auth_manager.py @@ -0,0 +1,78 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1 import fixture + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import auth + + +class AuthProjectsTest(utils.ClientTestCase): + + def setUp(self): + super(AuthProjectsTest, self).setUp() + + self.v3token = fixture.V3Token() + self.stub_auth(json=self.v3token) + + self.stub_url('GET', + [], + json={'version': fixture.V3Discovery(self.TEST_URL)}) + + def create_resource(self, id=None, name=None, **kwargs): + kwargs['id'] = id or uuid.uuid4().hex + kwargs['name'] = name or uuid.uuid4().hex + return kwargs + + def test_get_projects(self): + body = {'projects': [self.create_resource(), + self.create_resource(), + self.create_resource()]} + + self.stub_url('GET', ['auth', 'projects'], json=body) + + projects = self.client.auth.projects() + + self.assertEqual(3, len(projects)) + + for p in projects: + self.assertIsInstance(p, auth.Project) + + def test_get_domains(self): + body = {'domains': [self.create_resource(), + self.create_resource(), + self.create_resource()]} + + self.stub_url('GET', ['auth', 'domains'], json=body) + + domains = self.client.auth.domains() + + self.assertEqual(3, len(domains)) + + for d in domains: + self.assertIsInstance(d, auth.Domain) + + def test_get_systems(self): + body = {'system': [{ + 'all': True, + }]} + + self.stub_url('GET', ['auth', 'system'], json=body) + + systems = self.client.auth.systems() + system = systems[0] + + self.assertEqual(1, len(systems)) + self.assertIsInstance(system, auth.System) + self.assertTrue(system.all) diff --git a/keystoneclient/tests/unit/v3/test_auth_oidc.py b/keystoneclient/tests/unit/v3/test_auth_oidc.py new file mode 100644 index 000000000..278800bca --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_auth_oidc.py @@ -0,0 +1,189 @@ +# 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 urllib.parse +import uuid + +from oslo_config import fixture as config + +import testtools + +from keystoneclient.auth import conf +from keystoneclient.contrib.auth.v3 import oidc +from keystoneclient import session +from keystoneclient.tests.unit.v3 import utils + + +ACCESS_TOKEN_ENDPOINT_RESP = {"access_token": "z5H1ITZLlJVDHQXqJun", + "token_type": "bearer", + "expires_in": 3599, + "scope": "profile", + "refresh_token": "DCERsh83IAhu9bhavrp"} + +KEYSTONE_TOKEN_VALUE = uuid.uuid4().hex +UNSCOPED_TOKEN = { + "token": { + "issued_at": "2014-06-09T09:48:59.643406Z", + "extras": {}, + "methods": ["oidc"], + "expires_at": "2014-06-09T10:48:59.643375Z", + "user": { + "OS-FEDERATION": { + "identity_provider": { + "id": "bluepages" + }, + "protocol": { + "id": "oidc" + }, + "groups": [ + {"id": "1764fa5cf69a49a4918131de5ce4af9a"} + ] + }, + "id": "oidc_user%40example.com", + "name": "oidc_user@example.com" + } + } +} + + +class AuthenticateOIDCTests(utils.TestCase): + + GROUP = 'auth' + + def setUp(self): + super(AuthenticateOIDCTests, self).setUp() + + self.deprecations.expect_deprecations() + + self.conf_fixture = self.useFixture(config.Config()) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.session = session.Session() + + self.IDENTITY_PROVIDER = 'bluepages' + self.PROTOCOL = 'oidc' + self.USER_NAME = 'oidc_user@example.com' + self.PASSWORD = uuid.uuid4().hex + self.CLIENT_ID = uuid.uuid4().hex + self.CLIENT_SECRET = uuid.uuid4().hex + self.ACCESS_TOKEN_ENDPOINT = 'https://localhost:8020/oidc/token' + self.FEDERATION_AUTH_URL = '%s/%s' % ( + self.TEST_URL, + 'OS-FEDERATION/identity_providers/bluepages/protocols/oidc/auth') + + self.oidcplugin = oidc.OidcPassword( + self.TEST_URL, + self.IDENTITY_PROVIDER, + self.PROTOCOL, + username=self.USER_NAME, + password=self.PASSWORD, + client_id=self.CLIENT_ID, + client_secret=self.CLIENT_SECRET, + access_token_endpoint=self.ACCESS_TOKEN_ENDPOINT) + + @testtools.skip("TypeError: __init__() got an unexpected keyword" + " argument 'project_name'") + def test_conf_params(self): + """Ensure OpenID Connect config options work.""" + section = uuid.uuid4().hex + identity_provider = uuid.uuid4().hex + protocol = uuid.uuid4().hex + username = uuid.uuid4().hex + password = uuid.uuid4().hex + client_id = uuid.uuid4().hex + client_secret = uuid.uuid4().hex + access_token_endpoint = uuid.uuid4().hex + + self.conf_fixture.config(auth_section=section, group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.conf_fixture.register_opts(oidc.OidcPassword.get_options(), + group=section) + self.conf_fixture.config(auth_plugin='v3oidcpassword', + identity_provider=identity_provider, + protocol=protocol, + username=username, + password=password, + client_id=client_id, + client_secret=client_secret, + access_token_endpoint=access_token_endpoint, + group=section) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertEqual(identity_provider, a.identity_provider) + self.assertEqual(protocol, a.protocol) + self.assertEqual(username, a.username) + self.assertEqual(password, a.password) + self.assertEqual(client_id, a.client_id) + self.assertEqual(client_secret, a.client_secret) + self.assertEqual(access_token_endpoint, a.access_token_endpoint) + + def test_initial_call_to_get_access_token(self): + """Test initial call, expect JSON access token.""" + # Mock the output that creates the access token + self.requests_mock.post( + self.ACCESS_TOKEN_ENDPOINT, + json=ACCESS_TOKEN_ENDPOINT_RESP) + + # Prep all the values and send the request + grant_type = 'password' + scope = 'profile email' + client_auth = (self.CLIENT_ID, self.CLIENT_SECRET) + payload = {'grant_type': grant_type, 'username': self.USER_NAME, + 'password': self.PASSWORD, 'scope': scope} + res = self.oidcplugin._get_access_token(self.session, + client_auth, + payload, + self.ACCESS_TOKEN_ENDPOINT) + + # Verify the request matches the expected structure + self.assertEqual(self.ACCESS_TOKEN_ENDPOINT, res.request.url) + self.assertEqual('POST', res.request.method) + encoded_payload = urllib.parse.urlencode(payload) + self.assertEqual(encoded_payload, res.request.body) + + def test_second_call_to_protected_url(self): + """Test subsequent call, expect Keystone token.""" + # Mock the output that creates the keystone token + self.requests_mock.post( + self.FEDERATION_AUTH_URL, + json=UNSCOPED_TOKEN, + headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) + + # Prep all the values and send the request + access_token = uuid.uuid4().hex + headers = {'Authorization': 'Bearer ' + access_token} + res = self.oidcplugin._get_keystone_token(self.session, + headers, + self.FEDERATION_AUTH_URL) + + # Verify the request matches the expected structure + self.assertEqual(self.FEDERATION_AUTH_URL, res.request.url) + self.assertEqual('POST', res.request.method) + self.assertEqual(headers['Authorization'], + res.request.headers['Authorization']) + + def test_end_to_end_workflow(self): + """Test full OpenID Connect workflow.""" + # Mock the output that creates the access token + self.requests_mock.post( + self.ACCESS_TOKEN_ENDPOINT, + json=ACCESS_TOKEN_ENDPOINT_RESP) + + # Mock the output that creates the keystone token + self.requests_mock.post( + self.FEDERATION_AUTH_URL, + json=UNSCOPED_TOKEN, + headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) + + response = self.oidcplugin.get_unscoped_auth_ref(self.session) + self.assertEqual(KEYSTONE_TOKEN_VALUE, response.auth_token) diff --git a/keystoneclient/tests/unit/v3/test_auth_saml2.py b/keystoneclient/tests/unit/v3/test_auth_saml2.py new file mode 100644 index 000000000..8c2f67daa --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_auth_saml2.py @@ -0,0 +1,669 @@ +# 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 urllib.parse +import uuid + +from lxml import etree +from oslo_config import fixture as config +import requests + +from keystoneclient.auth import conf +from keystoneclient.contrib.auth.v3 import saml2 +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import saml2_fixtures +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib.federation import saml as saml_manager + +ROOTDIR = os.path.dirname(os.path.abspath(__file__)) +XMLDIR = os.path.join(ROOTDIR, 'examples', 'xml/') + + +def make_oneline(s): + return etree.tostring(etree.XML(s)).replace(b'\n', b'') + + +def _load_xml(filename): + with open(XMLDIR + filename, 'rb') as f: + return make_oneline(f.read()) + + +class AuthenticateviaSAML2Tests(utils.TestCase): + + GROUP = 'auth' + + class _AuthenticatedResponse(object): + headers = { + 'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER + } + + def json(self): + return saml2_fixtures.UNSCOPED_TOKEN + + class _AuthenticatedResponseInvalidJson(_AuthenticatedResponse): + + def json(self): + raise ValueError() + + class _AuthentiatedResponseMissingTokenID(_AuthenticatedResponse): + headers = {} + + def setUp(self): + super(AuthenticateviaSAML2Tests, self).setUp() + + self.deprecations.expect_deprecations() + + self.conf_fixture = self.useFixture(config.Config()) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.session = session.Session() + + self.ECP_SP_EMPTY_REQUEST_HEADERS = { + 'Accept': 'text/html; application/vnd.paos+xml', + 'PAOS': ('ver="urn:liberty:paos:2003-08";' + '"urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"') + } + + self.ECP_SP_SAML2_REQUEST_HEADERS = { + 'Content-Type': 'application/vnd.paos+xml' + } + + self.ECP_SAML2_NAMESPACES = { + 'ecp': 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp', + 'S': 'http://schemas.xmlsoap.org/soap/envelope/', + 'paos': 'urn:liberty:paos:2003-08' + } + self.ECP_RELAY_STATE = '//ecp:RelayState' + self.ECP_SERVICE_PROVIDER_CONSUMER_URL = ('/S:Envelope/S:Header/paos:' + 'Request/' + '@responseConsumerURL') + self.ECP_IDP_CONSUMER_URL = ('/S:Envelope/S:Header/ecp:Response/' + '@AssertionConsumerServiceURL') + self.IDENTITY_PROVIDER = 'testidp' + self.IDENTITY_PROVIDER_URL = 'http://local.url' + self.PROTOCOL = 'saml2' + self.FEDERATION_AUTH_URL = '%s/%s' % ( + self.TEST_URL, + 'OS-FEDERATION/identity_providers/testidp/protocols/saml2/auth') + self.SHIB_CONSUMER_URL = ('https://openstack4.local/' + 'Shibboleth.sso/SAML2/ECP') + + self.saml2plugin = saml2.Saml2UnscopedToken( + self.TEST_URL, + self.IDENTITY_PROVIDER, self.IDENTITY_PROVIDER_URL, + self.TEST_USER, self.TEST_TOKEN) + + def test_conf_params(self): + pass + + def test_initial_sp_call(self): + """Test initial call, expect SOAP message.""" + self.requests_mock.get( + self.FEDERATION_AUTH_URL, + content=make_oneline(saml2_fixtures.SP_SOAP_RESPONSE)) + a = self.saml2plugin._send_service_provider_request(self.session) + + self.assertFalse(a) + + fixture_soap_response = make_oneline( + saml2_fixtures.SP_SOAP_RESPONSE) + + sp_soap_response = make_oneline( + etree.tostring(self.saml2plugin.saml2_authn_request)) + + error_msg = "Expected %s instead of %s" % (fixture_soap_response, + sp_soap_response) + + self.assertEqual(fixture_soap_response, sp_soap_response, error_msg) + + self.assertEqual( + self.saml2plugin.sp_response_consumer_url, self.SHIB_CONSUMER_URL, + "Expected consumer_url set to %s instead of %s" % ( + self.SHIB_CONSUMER_URL, + str(self.saml2plugin.sp_response_consumer_url))) + + def test_initial_sp_call_when_saml_authenticated(self): + self.requests_mock.get( + self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) + + a = self.saml2plugin._send_service_provider_request(self.session) + self.assertTrue(a) + self.assertEqual( + saml2_fixtures.UNSCOPED_TOKEN['token'], + self.saml2plugin.authenticated_response.json()['token']) + self.assertEqual( + saml2_fixtures.UNSCOPED_TOKEN_HEADER, + self.saml2plugin.authenticated_response.headers['X-Subject-Token']) + + def test_get_unscoped_token_when_authenticated(self): + self.requests_mock.get( + self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER, + 'Content-Type': 'application/json'}) + + token, token_body = self.saml2plugin._get_unscoped_token(self.session) + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_body) + + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, token) + + def test_initial_sp_call_invalid_response(self): + """Send initial SP HTTP request and receive wrong server response.""" + self.requests_mock.get(self.FEDERATION_AUTH_URL, + text='NON XML RESPONSE') + + self.assertRaises( + exceptions.AuthorizationFailure, + self.saml2plugin._send_service_provider_request, + self.session) + + def test_send_authn_req_to_idp(self): + self.requests_mock.post(self.IDENTITY_PROVIDER_URL, + content=saml2_fixtures.SAML2_ASSERTION) + + self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL + self.saml2plugin.saml2_authn_request = etree.XML( + saml2_fixtures.SP_SOAP_RESPONSE) + self.saml2plugin._send_idp_saml2_authn_request(self.session) + + idp_response = make_oneline(etree.tostring( + self.saml2plugin.saml2_idp_authn_response)) + + saml2_assertion_oneline = make_oneline( + saml2_fixtures.SAML2_ASSERTION) + error = "Expected %s instead of %s" % (saml2_fixtures.SAML2_ASSERTION, + idp_response) + self.assertEqual(idp_response, saml2_assertion_oneline, error) + + def test_fail_basicauth_idp_authentication(self): + self.requests_mock.post(self.IDENTITY_PROVIDER_URL, status_code=401) + + self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL + self.saml2plugin.saml2_authn_request = etree.XML( + saml2_fixtures.SP_SOAP_RESPONSE) + self.assertRaises( + exceptions.Unauthorized, + self.saml2plugin._send_idp_saml2_authn_request, + self.session) + + def test_mising_username_password_in_plugin(self): + self.assertRaises(TypeError, + saml2.Saml2UnscopedToken, + self.TEST_URL, self.IDENTITY_PROVIDER, + self.IDENTITY_PROVIDER_URL) + + def test_send_authn_response_to_sp(self): + self.requests_mock.post( + self.SHIB_CONSUMER_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) + + self.saml2plugin.relay_state = etree.XML( + saml2_fixtures.SP_SOAP_RESPONSE).xpath( + self.ECP_RELAY_STATE, namespaces=self.ECP_SAML2_NAMESPACES)[0] + + self.saml2plugin.saml2_idp_authn_response = etree.XML( + saml2_fixtures.SAML2_ASSERTION) + + self.saml2plugin.idp_response_consumer_url = self.SHIB_CONSUMER_URL + self.saml2plugin._send_service_provider_saml2_authn_response( + self.session) + token_json = self.saml2plugin.authenticated_response.json()['token'] + token = self.saml2plugin.authenticated_response.headers[ + 'X-Subject-Token'] + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], + token_json) + + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, + token) + + def test_consumer_url_mismatch_success(self): + self.saml2plugin._check_consumer_urls( + self.session, self.SHIB_CONSUMER_URL, + self.SHIB_CONSUMER_URL) + + def test_consumer_url_mismatch(self): + self.requests_mock.post(self.SHIB_CONSUMER_URL) + invalid_consumer_url = uuid.uuid4().hex + self.assertRaises( + exceptions.ValidationError, + self.saml2plugin._check_consumer_urls, + self.session, self.SHIB_CONSUMER_URL, + invalid_consumer_url) + + def test_custom_302_redirection(self): + self.requests_mock.post( + self.SHIB_CONSUMER_URL, + text='BODY', + headers={'location': self.FEDERATION_AUTH_URL}, + status_code=302) + + self.requests_mock.get( + self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) + + self.session.redirect = False + response = self.session.post( + self.SHIB_CONSUMER_URL, data='CLIENT BODY') + self.assertEqual(302, response.status_code) + self.assertEqual(self.FEDERATION_AUTH_URL, + response.headers['location']) + + response = self.saml2plugin._handle_http_ecp_redirect( + self.session, response, 'GET') + + self.assertEqual(self.FEDERATION_AUTH_URL, response.request.url) + self.assertEqual('GET', response.request.method) + + def test_custom_303_redirection(self): + self.requests_mock.post( + self.SHIB_CONSUMER_URL, + text='BODY', + headers={'location': self.FEDERATION_AUTH_URL}, + status_code=303) + + self.requests_mock.get( + self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER}) + + self.session.redirect = False + response = self.session.post( + self.SHIB_CONSUMER_URL, data='CLIENT BODY') + self.assertEqual(303, response.status_code) + self.assertEqual(self.FEDERATION_AUTH_URL, + response.headers['location']) + + response = self.saml2plugin._handle_http_ecp_redirect( + self.session, response, 'GET') + + self.assertEqual(self.FEDERATION_AUTH_URL, response.request.url) + self.assertEqual('GET', response.request.method) + + def test_end_to_end_workflow(self): + self.requests_mock.get( + self.FEDERATION_AUTH_URL, + content=make_oneline(saml2_fixtures.SP_SOAP_RESPONSE)) + + self.requests_mock.post(self.IDENTITY_PROVIDER_URL, + content=saml2_fixtures.SAML2_ASSERTION) + + self.requests_mock.post( + self.SHIB_CONSUMER_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER, + 'Content-Type': 'application/json'}) + + self.session.redirect = False + response = self.saml2plugin.get_auth_ref(self.session) + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, + response.auth_token) + + +class ScopeFederationTokenTests(AuthenticateviaSAML2Tests): + + TEST_TOKEN = client_fixtures.AUTH_SUBJECT_TOKEN + + def setUp(self): + super(ScopeFederationTokenTests, self).setUp() + + self.PROJECT_SCOPED_TOKEN_JSON = client_fixtures.project_scoped_token() + self.PROJECT_SCOPED_TOKEN_JSON['methods'] = ['saml2'] + + # for better readability + self.TEST_TENANT_ID = self.PROJECT_SCOPED_TOKEN_JSON.project_id + self.TEST_TENANT_NAME = self.PROJECT_SCOPED_TOKEN_JSON.project_name + + self.DOMAIN_SCOPED_TOKEN_JSON = client_fixtures.domain_scoped_token() + self.DOMAIN_SCOPED_TOKEN_JSON['methods'] = ['saml2'] + + # for better readability + self.TEST_DOMAIN_ID = self.DOMAIN_SCOPED_TOKEN_JSON.domain_id + self.TEST_DOMAIN_NAME = self.DOMAIN_SCOPED_TOKEN_JSON.domain_name + + self.saml2_scope_plugin = saml2.Saml2ScopedToken( + self.TEST_URL, saml2_fixtures.UNSCOPED_TOKEN_HEADER, + project_id=self.TEST_TENANT_ID) + + def test_scope_saml2_token_to_project(self): + self.stub_auth(json=self.PROJECT_SCOPED_TOKEN_JSON) + + token = self.saml2_scope_plugin.get_auth_ref(self.session) + self.assertTrue(token.project_scoped, "Received token is not scoped") + self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token) + self.assertEqual(self.TEST_TENANT_ID, token.project_id) + self.assertEqual(self.TEST_TENANT_NAME, token.project_name) + + def test_scope_saml2_token_to_invalid_project(self): + self.stub_auth(status_code=401) + self.saml2_scope_plugin.project_id = uuid.uuid4().hex + self.saml2_scope_plugin.project_name = None + self.assertRaises(exceptions.Unauthorized, + self.saml2_scope_plugin.get_auth_ref, + self.session) + + def test_scope_saml2_token_to_invalid_domain(self): + self.stub_auth(status_code=401) + self.saml2_scope_plugin.project_id = None + self.saml2_scope_plugin.project_name = None + self.saml2_scope_plugin.domain_id = uuid.uuid4().hex + self.saml2_scope_plugin.domain_name = None + self.assertRaises(exceptions.Unauthorized, + self.saml2_scope_plugin.get_auth_ref, + self.session) + + def test_scope_saml2_token_to_domain(self): + self.stub_auth(json=self.DOMAIN_SCOPED_TOKEN_JSON) + token = self.saml2_scope_plugin.get_auth_ref(self.session) + self.assertTrue(token.domain_scoped, "Received token is not scoped") + self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token) + self.assertEqual(self.TEST_DOMAIN_ID, token.domain_id) + self.assertEqual(self.TEST_DOMAIN_NAME, token.domain_name) + + def test_dont_set_project_nor_domain(self): + self.saml2_scope_plugin.project_id = None + self.saml2_scope_plugin.domain_id = None + self.assertRaises(exceptions.ValidationError, + saml2.Saml2ScopedToken, + self.TEST_URL, client_fixtures.AUTH_SUBJECT_TOKEN) + + +class AuthenticateviaADFSTests(utils.TestCase): + + GROUP = 'auth' + + NAMESPACES = { + 's': 'http://www.w3.org/2003/05/soap-envelope', + 'trust': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512', + 'wsa': 'http://www.w3.org/2005/08/addressing', + 'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy', + 'a': 'http://www.w3.org/2005/08/addressing', + 'o': ('http://docs.oasis-open.org/wss/2004/01/oasis' + '-200401-wss-wssecurity-secext-1.0.xsd') + } + + USER_XPATH = ('/s:Envelope/s:Header' + '/o:Security' + '/o:UsernameToken' + '/o:Username') + PASSWORD_XPATH = ('/s:Envelope/s:Header' + '/o:Security' + '/o:UsernameToken' + '/o:Password') + ADDRESS_XPATH = ('/s:Envelope/s:Body' + '/trust:RequestSecurityToken' + '/wsp:AppliesTo/wsa:EndpointReference' + '/wsa:Address') + TO_XPATH = ('/s:Envelope/s:Header' + '/a:To') + + @property + def _uuid4(self): + return '4b911420-4982-4009-8afc-5c596cd487f5' + + def setUp(self): + super(AuthenticateviaADFSTests, self).setUp() + + self.deprecations.expect_deprecations() + + self.conf_fixture = self.useFixture(config.Config()) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.session = session.Session(session=requests.Session()) + + self.IDENTITY_PROVIDER = 'adfs' + self.IDENTITY_PROVIDER_URL = ('http://adfs.local/adfs/service/trust/13' + '/usernamemixed') + self.FEDERATION_AUTH_URL = '%s/%s' % ( + self.TEST_URL, + 'OS-FEDERATION/identity_providers/adfs/protocols/saml2/auth') + self.SP_ENDPOINT = 'https://openstack4.local/Shibboleth.sso/ADFS' + + self.adfsplugin = saml2.ADFSUnscopedToken( + self.TEST_URL, self.IDENTITY_PROVIDER, + self.IDENTITY_PROVIDER_URL, self.SP_ENDPOINT, + self.TEST_USER, self.TEST_TOKEN) + + self.ADFS_SECURITY_TOKEN_RESPONSE = _load_xml( + 'ADFS_RequestSecurityTokenResponse.xml') + self.ADFS_FAULT = _load_xml('ADFS_fault.xml') + + def test_conf_params(self): + pass + + def test_get_adfs_security_token(self): + """Test ADFSUnscopedToken._get_adfs_security_token().""" + self.requests_mock.post( + self.IDENTITY_PROVIDER_URL, + content=make_oneline(self.ADFS_SECURITY_TOKEN_RESPONSE), + status_code=200) + + self.adfsplugin._prepare_adfs_request() + self.adfsplugin._get_adfs_security_token(self.session) + + adfs_response = etree.tostring(self.adfsplugin.adfs_token) + fixture_response = self.ADFS_SECURITY_TOKEN_RESPONSE + + self.assertEqual(fixture_response, adfs_response) + + def test_adfs_request_user(self): + self.adfsplugin._prepare_adfs_request() + user = self.adfsplugin.prepared_request.xpath( + self.USER_XPATH, namespaces=self.NAMESPACES)[0] + self.assertEqual(self.TEST_USER, user.text) + + def test_adfs_request_password(self): + self.adfsplugin._prepare_adfs_request() + password = self.adfsplugin.prepared_request.xpath( + self.PASSWORD_XPATH, namespaces=self.NAMESPACES)[0] + self.assertEqual(self.TEST_TOKEN, password.text) + + def test_adfs_request_to(self): + self.adfsplugin._prepare_adfs_request() + to = self.adfsplugin.prepared_request.xpath( + self.TO_XPATH, namespaces=self.NAMESPACES)[0] + self.assertEqual(self.IDENTITY_PROVIDER_URL, to.text) + + def test_prepare_adfs_request_address(self): + self.adfsplugin._prepare_adfs_request() + address = self.adfsplugin.prepared_request.xpath( + self.ADDRESS_XPATH, namespaces=self.NAMESPACES)[0] + self.assertEqual(self.SP_ENDPOINT, address.text) + + def test_prepare_sp_request(self): + assertion = etree.XML(self.ADFS_SECURITY_TOKEN_RESPONSE) + assertion = assertion.xpath( + saml2.ADFSUnscopedToken.ADFS_ASSERTION_XPATH, + namespaces=saml2.ADFSUnscopedToken.ADFS_TOKEN_NAMESPACES) + assertion = assertion[0] + assertion = etree.tostring(assertion) + + assertion = assertion.replace( + b'http://docs.oasis-open.org/ws-sx/ws-trust/200512', + b'http://schemas.xmlsoap.org/ws/2005/02/trust') + assertion = urllib.parse.quote(assertion) + assertion = 'wa=wsignin1.0&wresult=' + assertion + + self.adfsplugin.adfs_token = etree.XML( + self.ADFS_SECURITY_TOKEN_RESPONSE) + self.adfsplugin._prepare_sp_request() + + self.assertEqual(assertion, self.adfsplugin.encoded_assertion) + + def test_get_adfs_security_token_authn_fail(self): + """Test proper parsing XML fault after bad authentication. + + An exceptions.AuthorizationFailure should be raised including + error message from the XML message indicating where was the problem. + """ + self.requests_mock.post(self.IDENTITY_PROVIDER_URL, + content=make_oneline(self.ADFS_FAULT), + status_code=500) + + self.adfsplugin._prepare_adfs_request() + self.assertRaises(exceptions.AuthorizationFailure, + self.adfsplugin._get_adfs_security_token, + self.session) + # TODO(marek-denis): Python3 tests complain about missing 'message' + # attributes + # self.assertEqual('a:FailedAuthentication', e.message) + + def test_get_adfs_security_token_bad_response(self): + """Test proper handling HTTP 500 and mangled (non XML) response. + + This should never happen yet, keystoneclient should be prepared + and correctly raise exceptions.InternalServerError once it cannot + parse XML fault message + """ + self.requests_mock.post(self.IDENTITY_PROVIDER_URL, + content=b'NOT XML', + status_code=500) + self.adfsplugin._prepare_adfs_request() + self.assertRaises(exceptions.InternalServerError, + self.adfsplugin._get_adfs_security_token, + self.session) + + # TODO(marek-denis): Need to figure out how to properly send cookies + # from the request_uri() method. + def _send_assertion_to_service_provider(self): + """Test whether SP issues a cookie.""" + cookie = uuid.uuid4().hex + + self.requests_mock.post(self.SP_ENDPOINT, + headers={"set-cookie": cookie}, + status_code=302) + + self.adfsplugin.adfs_token = self._build_adfs_request() + self.adfsplugin._prepare_sp_request() + self.adfsplugin._send_assertion_to_service_provider(self.session) + + self.assertEqual(1, len(self.session.session.cookies)) + + def test_send_assertion_to_service_provider_bad_status(self): + self.requests_mock.post(self.SP_ENDPOINT, status_code=500) + + self.adfsplugin.adfs_token = etree.XML( + self.ADFS_SECURITY_TOKEN_RESPONSE) + self.adfsplugin._prepare_sp_request() + + self.assertRaises( + exceptions.InternalServerError, + self.adfsplugin._send_assertion_to_service_provider, + self.session) + + def test_access_sp_no_cookies_fail(self): + # There are no cookies in the session initially, and + # _access_service_provider requires a cookie in the session. + self.assertRaises(exceptions.AuthorizationFailure, + self.adfsplugin._access_service_provider, + self.session) + + def test_check_valid_token_when_authenticated(self): + self.requests_mock.get(self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers=client_fixtures.AUTH_RESPONSE_HEADERS) + + # _access_service_provider requires a cookie in the session. + cookie = requests.cookies.create_cookie( + name=self.getUniqueString(), value=self.getUniqueString()) + self.session.session.cookies.set_cookie(cookie) + + self.adfsplugin._access_service_provider(self.session) + response = self.adfsplugin.authenticated_response + + self.assertEqual(client_fixtures.AUTH_RESPONSE_HEADERS, + response.headers) + + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], + response.json()['token']) + + def test_end_to_end_workflow(self): + self.requests_mock.post(self.IDENTITY_PROVIDER_URL, + content=self.ADFS_SECURITY_TOKEN_RESPONSE, + status_code=200) + self.requests_mock.post(self.SP_ENDPOINT, + headers={"set-cookie": 'x'}, + status_code=302) + self.requests_mock.get(self.FEDERATION_AUTH_URL, + json=saml2_fixtures.UNSCOPED_TOKEN, + headers=client_fixtures.AUTH_RESPONSE_HEADERS) + + # NOTE(marek-denis): We need to mimic this until self.requests_mock can + # issue cookies properly. + cookie = requests.cookies.create_cookie( + name=self.getUniqueString(), value=self.getUniqueString()) + self.session.session.cookies.set_cookie(cookie) + + token, token_json = self.adfsplugin._get_unscoped_token(self.session) + self.assertEqual(token, client_fixtures.AUTH_SUBJECT_TOKEN) + self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_json) + + +class SAMLGenerationTests(utils.ClientTestCase): + + def setUp(self): + super(SAMLGenerationTests, self).setUp() + self.manager = self.client.federation.saml + self.SAML2_FULL_URL = ''.join([self.TEST_URL, + saml_manager.SAML2_ENDPOINT]) + self.ECP_FULL_URL = ''.join([self.TEST_URL, + saml_manager.ECP_ENDPOINT]) + + def test_saml_create(self): + """Test that a token can be exchanged for a SAML assertion.""" + token_id = uuid.uuid4().hex + service_provider_id = uuid.uuid4().hex + + # Mock the returned text for '/auth/OS-FEDERATION/saml2 + self.requests_mock.post(self.SAML2_FULL_URL, + text=saml2_fixtures.TOKEN_BASED_SAML) + + text = self.manager.create_saml_assertion(service_provider_id, + token_id) + + # Ensure returned text is correct + self.assertEqual(saml2_fixtures.TOKEN_BASED_SAML, text) + + # Ensure request headers and body are correct + req_json = self.requests_mock.last_request.json() + self.assertEqual(token_id, req_json['auth']['identity']['token']['id']) + self.assertEqual(service_provider_id, + req_json['auth']['scope']['service_provider']['id']) + self.assertRequestHeaderEqual('Content-Type', 'application/json') + + def test_ecp_create(self): + """Test that a token can be exchanged for an ECP wrapped assertion.""" + token_id = uuid.uuid4().hex + service_provider_id = uuid.uuid4().hex + + # Mock returned text for '/auth/OS-FEDERATION/saml2/ecp + self.requests_mock.post(self.ECP_FULL_URL, + text=saml2_fixtures.TOKEN_BASED_ECP) + + text = self.manager.create_ecp_assertion(service_provider_id, + token_id) + + # Ensure returned text is correct + self.assertEqual(saml2_fixtures.TOKEN_BASED_ECP, text) + + # Ensure request headers and body are correct + req_json = self.requests_mock.last_request.json() + self.assertEqual(token_id, req_json['auth']['identity']['token']['id']) + self.assertEqual(service_provider_id, + req_json['auth']['scope']['service_provider']['id']) + self.assertRequestHeaderEqual('Content-Type', 'application/json') diff --git a/keystoneclient/tests/unit/v3/test_client.py b/keystoneclient/tests/unit/v3/test_client.py new file mode 100644 index 000000000..82088fdfc --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_client.py @@ -0,0 +1,270 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import uuid + +from oslo_serialization import jsonutils + +from keystoneauth1 import session as auth_session +from keystoneclient.auth import token_endpoint +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import client + + +class KeystoneClientTest(utils.TestCase): + + def test_unscoped_init(self): + token = client_fixtures.unscoped_token() + self.stub_auth(json=token) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + c = client.Client(user_domain_name=token.user_domain_name, + username=token.user_name, + password='password', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertEqual(token.user_id, c.auth_user_id) + self.assertFalse(c.has_service_catalog()) + + self.assertEqual(token.user_id, c.get_user_id(session=None)) + self.assertIsNone(c.get_project_id(session=None)) + + def test_domain_scoped_init(self): + token = client_fixtures.domain_scoped_token() + self.stub_auth(json=token) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + c = client.Client(user_id=token.user_id, + password='password', + domain_name=token.domain_name, + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertTrue(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertEqual(token.user_id, c.auth_user_id) + self.assertEqual(token.domain_id, c.auth_domain_id) + + def test_project_scoped_init(self): + token = client_fixtures.project_scoped_token() + self.stub_auth(json=token), + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + c = client.Client(user_id=token.user_id, + password='password', + user_domain_name=token.user_domain_name, + project_name=token.project_name, + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertTrue(c.auth_ref.project_scoped) + self.assertEqual(token.user_id, c.auth_user_id) + self.assertEqual(token.project_id, c.auth_tenant_id) + self.assertEqual(token.user_id, c.get_user_id(session=None)) + self.assertEqual(token.project_id, c.get_project_id(session=None)) + + def test_auth_ref_load(self): + token = client_fixtures.project_scoped_token() + self.stub_auth(json=token) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + c = client.Client(user_id=token.user_id, + password='password', + project_id=token.project_id, + auth_url=self.TEST_URL) + cache = jsonutils.dumps(c.auth_ref) + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + new_client = client.Client(auth_ref=jsonutils.loads(cache)) + self.assertIsNotNone(new_client.auth_ref) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertEqual(token.user_name, new_client.username) + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v3') + + def test_auth_ref_load_with_overridden_arguments(self): + new_auth_url = 'https://newkeystone.com/v3' + + user_id = uuid.uuid4().hex + user_name = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + first = client_fixtures.project_scoped_token(user_id=user_id, + user_name=user_name, + project_id=project_id) + second = client_fixtures.project_scoped_token(user_id=user_id, + user_name=user_name, + project_id=project_id) + self.stub_auth(json=first) + self.stub_auth(json=second, base_url=new_auth_url) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + c = client.Client(user_id=user_id, + password='password', + project_id=project_id, + auth_url=self.TEST_URL) + cache = jsonutils.dumps(c.auth_ref) + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + new_client = client.Client(auth_ref=jsonutils.loads(cache), + auth_url=new_auth_url) + self.assertIsNotNone(new_client.auth_ref) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertEqual(new_auth_url, new_client.auth_url) + self.assertEqual(user_name, new_client.username) + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v3') + + def test_trust_init(self): + token = client_fixtures.trust_token() + self.stub_auth(json=token) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + c = client.Client(user_domain_name=token.user_domain_name, + username=token.user_name, + password='password', + auth_url=self.TEST_URL, + trust_id=token.trust_id) + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertEqual(token.trust_id, c.auth_ref.trust_id) + self.assertEqual(token.trustee_user_id, c.auth_ref.trustee_user_id) + self.assertEqual(token.trustor_user_id, c.auth_ref.trustor_user_id) + self.assertTrue(c.auth_ref.trust_scoped) + self.assertEqual(token.user_id, c.auth_user_id) + + def test_init_err_no_auth_url(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + self.assertRaises(exceptions.AuthorizationFailure, + client.Client, + username='exampleuser', + password='password') + + def _management_url_is_updated(self, fixture, **kwargs): + second = copy.deepcopy(fixture) + first_url = 'http://admin:35357/v3' + second_url = "http://secondurl:%d/v3'" + + for entry in second['token']['catalog']: + if entry['type'] == 'identity': + entry['endpoints'] = [{ + 'url': second_url % 5000, + 'region': 'RegionOne', + 'interface': 'public' + }, { + 'url': second_url % 5000, + 'region': 'RegionOne', + 'interface': 'internal' + }, { + 'url': second_url % 35357, + 'region': 'RegionOne', + 'interface': 'admin' + }] + + self.stub_auth(response_list=[{'json': fixture}, {'json': second}]) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = client.Client(username='exampleuser', + password='password', + auth_url=self.TEST_URL, + **kwargs) + self.assertEqual(cl.management_url, first_url) + + with self.deprecations.expect_deprecations_here(): + cl.authenticate() + self.assertEqual(cl.management_url, second_url % 35357) + + def test_management_url_is_updated_with_project(self): + self._management_url_is_updated(client_fixtures.project_scoped_token(), + project_name='exampleproject') + + def test_management_url_is_updated_with_domain(self): + self._management_url_is_updated(client_fixtures.domain_scoped_token(), + domain_name='exampledomain') + + def test_client_with_region_name_passes_to_service_catalog(self): + # NOTE(jamielennox): this is deprecated behaviour that should be + # removed ASAP, however must remain compatible. + self.deprecations.expect_deprecations() + + self.stub_auth(json=client_fixtures.auth_response_body()) + + cl = client.Client(username='exampleuser', + password='password', + project_name='exampleproject', + auth_url=self.TEST_URL, + region_name='North') + self.assertEqual(cl.service_catalog.url_for(service_type='image'), + 'http://glance.north.host/glanceapi/public') + + cl = client.Client(username='exampleuser', + password='password', + project_name='exampleproject', + auth_url=self.TEST_URL, + region_name='South') + self.assertEqual(cl.service_catalog.url_for(service_type='image'), + 'http://glance.south.host/glanceapi/public') + + def test_client_without_auth_params(self): + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + self.assertRaises(exceptions.AuthorizationFailure, + client.Client, + project_name='exampleproject', + auth_url=self.TEST_URL) + + def test_client_params(self): + with self.deprecations.expect_deprecations_here(): + sess = session.Session() + auth = token_endpoint.Token('a', 'b') + + opts = {'auth': auth, + 'connect_retries': 50, + 'endpoint_override': uuid.uuid4().hex, + 'interface': uuid.uuid4().hex, + 'region_name': uuid.uuid4().hex, + 'service_name': uuid.uuid4().hex, + 'user_agent': uuid.uuid4().hex, + } + + cl = client.Client(session=sess, **opts) + + for k, v in opts.items(): + self.assertEqual(v, getattr(cl._adapter, k)) + + self.assertEqual('identity', cl._adapter.service_type) + self.assertEqual((3, 0), cl._adapter.version) + + def test_empty_service_catalog_param(self): + # Client().service_catalog should return None if the client is not + # authenticated + sess = auth_session.Session() + cl = client.Client(session=sess) + self.assertIsNone(cl.service_catalog) diff --git a/keystoneclient/tests/unit/v3/test_credentials.py b/keystoneclient/tests/unit/v3/test_credentials.py new file mode 100644 index 000000000..0efc3dfbf --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_credentials.py @@ -0,0 +1,33 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import credentials + + +class CredentialTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(CredentialTests, self).setUp() + self.key = 'credential' + self.collection_key = 'credentials' + self.model = credentials.Credential + self.manager = self.client.credentials + + def new_ref(self, **kwargs): + kwargs = super(CredentialTests, self).new_ref(**kwargs) + kwargs.setdefault('blob', uuid.uuid4().hex) + kwargs.setdefault('project_id', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('user_id', uuid.uuid4().hex) + return kwargs diff --git a/keystoneclient/tests/unit/v3/test_discover.py b/keystoneclient/tests/unit/v3/test_discover.py new file mode 100644 index 000000000..f54b2f9c4 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_discover.py @@ -0,0 +1,82 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.generic import client +from keystoneclient.tests.unit.v3 import utils + + +class DiscoverKeystoneTests(utils.UnauthenticatedTestCase): + def setUp(self): + super(DiscoverKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "versions": { + "values": [{"id": "v3.0", + "status": "beta", + "updated": "2013-03-06T00:00:00Z", + "links": [ + {"rel": "self", + "href": "http://127.0.0.1:5000/v3.0/", }, + {"rel": "describedby", + "type": "text/html", + "href": "https://docs.openstack.org/api/" + "openstack-identity-service/3/" + "content/", }, + {"rel": "describedby", + "type": "application/pdf", + "href": "https://docs.openstack.org/api/" + "openstack-identity-service/3/" + "identity-dev-guide-3.pdf", }, + ]}, + {"id": "v2.0", + "status": "beta", + "updated": "2013-03-06T00:00:00Z", + "links": [ + {"rel": "self", + "href": "http://127.0.0.1:5000/v2.0/", }, + {"rel": "describedby", + "type": "text/html", + "href": "https://docs.openstack.org/api/" + "openstack-identity-service/2.0/" + "content/", }, + {"rel": "describedby", + "type": "application/pdf", + "href": "https://docs.openstack.org/api/" + "openstack-identity-service/2.0/" + "identity-dev-guide-2.0.pdf", } + ]}], + }, + } + self.TEST_REQUEST_HEADERS = { + 'User-Agent': 'python-keystoneclient', + 'Accept': 'application/json', + } + + def test_get_version_local(self): + self.requests_mock.get("http://localhost:35357/", + status_code=300, + json=self.TEST_RESPONSE_DICT) + + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cs = client.Client() + versions = cs.discover() + self.assertIsInstance(versions, dict) + self.assertIn('message', versions) + self.assertIn('v3.0', versions) + self.assertEqual( + versions['v3.0']['url'], + self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0] + ['href']) + self.assertEqual( + versions['v2.0']['url'], + self.TEST_RESPONSE_DICT['versions']['values'][1]['links'][0] + ['href']) diff --git a/keystoneclient/tests/unit/v3/test_domain_configs.py b/keystoneclient/tests/unit/v3/test_domain_configs.py new file mode 100644 index 000000000..2a7df0927 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_domain_configs.py @@ -0,0 +1,96 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import domain_configs + + +class DomainConfigsTests(utils.ClientTestCase, utils.CrudTests): + """Test domain config database management.""" + + def setUp(self): + super(DomainConfigsTests, self).setUp() + self.key = 'config' + self.model = domain_configs.DomainConfig + self.manager = self.client.domain_configs + + def new_ref(self, **kwargs): + config_groups = {'identity': {uuid.uuid4().hex: uuid.uuid4().hex}, + 'ldap': {uuid.uuid4().hex: uuid.uuid4().hex}} + kwargs.setdefault('config', config_groups) + return kwargs + + def _assert_resource_attributes(self, resource, req_ref): + for attr in req_ref: + self.assertEqual( + getattr(resource, attr), + req_ref[attr], + 'Expected different %s' % attr) + + def test_create(self): + domain_id = uuid.uuid4().hex + config = self.new_ref() + + self.stub_url('PUT', + parts=['domains', domain_id, 'config'], + json=config, status_code=201) + res = self.manager.create(domain_id, config) + self._assert_resource_attributes(res, config['config']) + self.assertEntityRequestBodyIs(config) + + def test_update(self): + domain_id = uuid.uuid4().hex + config = self.new_ref() + + self.stub_url('PATCH', + parts=['domains', domain_id, 'config'], + json=config, status_code=200) + res = self.manager.update(domain_id, config) + self._assert_resource_attributes(res, config['config']) + self.assertEntityRequestBodyIs(config) + + def test_get(self): + domain_id = uuid.uuid4().hex + config = self.new_ref() + config = config['config'] + + self.stub_entity('GET', + parts=['domains', domain_id, 'config'], + entity=config) + res = self.manager.get(domain_id) + self._assert_resource_attributes(res, config) + + def test_delete(self): + domain_id = uuid.uuid4().hex + self.stub_url('DELETE', + parts=['domains', domain_id, 'config'], + status_code=204) + self.manager.delete(domain_id) + + def test_list(self): + # List not supported for domain config + self.assertRaises(exceptions.MethodNotImplemented, self.manager.list) + + def test_list_by_id(self): + # List not supported for domain config + self.assertRaises(exceptions.MethodNotImplemented, self.manager.list) + + def test_list_params(self): + # List not supported for domain config + self.assertRaises(exceptions.MethodNotImplemented, self.manager.list) + + def test_find(self): + # Find not supported for domain config + self.assertRaises(exceptions.MethodNotImplemented, self.manager.find) diff --git a/keystoneclient/tests/unit/v3/test_domains.py b/keystoneclient/tests/unit/v3/test_domains.py new file mode 100644 index 000000000..72a7b8833 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_domains.py @@ -0,0 +1,53 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import domains + + +class DomainTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(DomainTests, self).setUp() + self.key = 'domain' + self.collection_key = 'domains' + self.model = domains.Domain + self.manager = self.client.domains + + def new_ref(self, **kwargs): + kwargs = super(DomainTests, self).new_ref(**kwargs) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_filter_for_default_domain_by_id(self): + ref = self.new_ref(id='default') + super(DomainTests, self).test_list_by_id( + ref=ref, + id=ref['id']) + + def test_list_filter_name(self): + super(DomainTests, self).test_list(name='adomain123') + + def test_list_filter_enabled(self): + super(DomainTests, self).test_list(enabled=True) + + def test_list_filter_disabled(self): + # False is converted to '0' ref bug #1267530 + expected_query = {'enabled': '0'} + super(DomainTests, self).test_list(expected_query=expected_query, + enabled=False) + + def test_update_enabled_defaults_to_none(self): + super(DomainTests, self).test_update( + req_ref={'name': uuid.uuid4().hex}) diff --git a/keystoneclient/tests/unit/v3/test_ec2.py b/keystoneclient/tests/unit/v3/test_ec2.py new file mode 100644 index 000000000..66679f605 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_ec2.py @@ -0,0 +1,107 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import ec2 + + +class EC2Tests(utils.ClientTestCase): + + def test_create(self): + user_id = 'usr' + tenant_id = 'tnt' + req_body = { + "tenant_id": tenant_id, + } + resp_body = { + "credential": { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + } + } + self.stub_url('POST', ['users', user_id, 'credentials', + 'OS-EC2'], json=resp_body) + + cred = self.client.ec2.create(user_id, tenant_id) + self.assertIsInstance(cred, ec2.EC2) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + self.assertRequestBodyIs(json=req_body) + + def test_get(self): + user_id = 'usr' + tenant_id = 'tnt' + resp_body = { + "credential": { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + } + } + self.stub_url('GET', ['users', user_id, 'credentials', + 'OS-EC2', 'access'], json=resp_body) + + cred = self.client.ec2.get(user_id, 'access') + self.assertIsInstance(cred, ec2.EC2) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + + def test_list(self): + user_id = 'usr' + tenant_id = 'tnt' + resp_body = { + "credentials": { + "values": [ + { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + }, + { + "access": "another", + "secret": "key", + "tenant_id": tenant_id, + "created": "12/12/31", + "enabled": True, + } + ] + } + } + self.stub_url('GET', ['users', user_id, 'credentials', + 'OS-EC2'], json=resp_body) + + creds = self.client.ec2.list(user_id) + self.assertEqual(len(creds), 2) + cred = creds[0] + self.assertIsInstance(cred, ec2.EC2) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + + def test_delete(self): + user_id = 'usr' + access = 'access' + self.stub_url('DELETE', ['users', user_id, 'credentials', + 'OS-EC2', access], status_code=204) + self.client.ec2.delete(user_id, access) diff --git a/keystoneclient/tests/unit/v3/test_endpoint_filter.py b/keystoneclient/tests/unit/v3/test_endpoint_filter.py new file mode 100644 index 000000000..62e89cb35 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_endpoint_filter.py @@ -0,0 +1,293 @@ +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v3 import utils + + +class EndpointTestUtils(object): + """Mixin class with shared methods between Endpoint Filter & Policy.""" + + def new_ref(self, **kwargs): + # copied from CrudTests as we need to create endpoint and project + # refs for our tests. EndpointFilter is not exactly CRUD API. + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs + + def new_endpoint_ref(self, **kwargs): + # copied from EndpointTests as we need endpoint refs for our tests + kwargs = self.new_ref(**kwargs) + kwargs.setdefault('interface', 'public') + kwargs.setdefault('region', uuid.uuid4().hex) + kwargs.setdefault('service_id', uuid.uuid4().hex) + kwargs.setdefault('url', uuid.uuid4().hex) + return kwargs + + def new_endpoint_group_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('filters') + return kwargs + + +class EndpointFilterTests(utils.ClientTestCase, EndpointTestUtils): + """Test project-endpoint associations (a.k.a. EndpointFilter Extension). + + Endpoint filter provides associations between service endpoints and + projects. These assciations are then used to create ad-hoc catalogs for + each project-scoped token request. + + """ + + def setUp(self): + super(EndpointFilterTests, self).setUp() + self.manager = self.client.endpoint_filter + + def new_project_ref(self, **kwargs): + # copied from ProjectTests as we need project refs for our tests + kwargs = self.new_ref(**kwargs) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_add_endpoint_to_project_via_id(self): + endpoint_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('PUT', + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints', endpoint_id], + status_code=201) + + self.manager.add_endpoint_to_project(project=project_id, + endpoint=endpoint_id) + + def test_add_endpoint_to_project_via_obj(self): + project_ref = self.new_project_ref() + endpoint_ref = self.new_endpoint_ref() + project = self.client.projects.resource_class(self.client.projects, + project_ref, + loaded=True) + endpoint = self.client.endpoints.resource_class(self.client.endpoints, + endpoint_ref, + loaded=True) + + self.stub_url('PUT', + [self.manager.OS_EP_FILTER_EXT, + 'projects', project_ref['id'], + 'endpoints', endpoint_ref['id']], + status_code=201) + + self.manager.add_endpoint_to_project(project=project, + endpoint=endpoint) + + def test_delete_endpoint_from_project(self): + endpoint_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('DELETE', + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints', endpoint_id], + status_code=201) + + self.manager.delete_endpoint_from_project(project=project_id, + endpoint=endpoint_id) + + def test_check_endpoint_in_project(self): + endpoint_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('HEAD', + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints', endpoint_id], + status_code=201) + + self.manager.check_endpoint_in_project(project=project_id, + endpoint=endpoint_id) + + def test_list_endpoints_for_project(self): + project_id = uuid.uuid4().hex + endpoints = {'endpoints': [self.new_endpoint_ref(), + self.new_endpoint_ref()]} + self.stub_url('GET', + [self.manager.OS_EP_FILTER_EXT, 'projects', project_id, + 'endpoints'], + json=endpoints, + status_code=200) + + endpoints_resp = self.manager.list_endpoints_for_project( + project=project_id) + + expected_endpoint_ids = [ + endpoint['id'] for endpoint in endpoints['endpoints']] + actual_endpoint_ids = [endpoint.id for endpoint in endpoints_resp] + self.assertEqual(expected_endpoint_ids, actual_endpoint_ids) + + def test_list_projects_for_endpoint(self): + endpoint_id = uuid.uuid4().hex + projects = {'projects': [self.new_project_ref(), + self.new_project_ref()]} + self.stub_url('GET', + [self.manager.OS_EP_FILTER_EXT, 'endpoints', endpoint_id, + 'projects'], + json=projects, + status_code=200) + + projects_resp = self.manager.list_projects_for_endpoint( + endpoint=endpoint_id) + + expected_project_ids = [ + project['id'] for project in projects['projects']] + actual_project_ids = [project.id for project in projects_resp] + self.assertEqual(expected_project_ids, actual_project_ids) + + def test_list_projects_for_endpoint_group(self): + endpoint_group_id = uuid.uuid4().hex + projects = {'projects': [self.new_project_ref(), + self.new_project_ref()]} + self.stub_url('GET', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects'], + json=projects, + status_code=200) + + projects_resp = self.manager.list_projects_for_endpoint_group( + endpoint_group=endpoint_group_id) + + expected_project_ids = [ + project['id'] for project in projects['projects']] + actual_project_ids = [project.id for project in projects_resp] + self.assertEqual(expected_project_ids, actual_project_ids) + + def test_list_projects_for_endpoint_group_value_error(self): + self.assertRaises(ValueError, + self.manager.list_projects_for_endpoint_group, + endpoint_group='') + self.assertRaises(ValueError, + self.manager.list_projects_for_endpoint_group, + endpoint_group=None) + + def test_list_endpoint_groups_for_project(self): + project_id = uuid.uuid4().hex + endpoint_groups = { + 'endpoint_groups': [self.new_endpoint_group_ref(), + self.new_endpoint_group_ref()]} + self.stub_url('GET', + [self.manager.OS_EP_FILTER_EXT, 'projects', + project_id, 'endpoint_groups'], + json=endpoint_groups, + status_code=200) + + endpoint_groups_resp = self.manager.list_endpoint_groups_for_project( + project=project_id) + + expected_endpoint_group_ids = [ + endpoint_group['id'] for endpoint_group + in endpoint_groups['endpoint_groups'] + ] + actual_endpoint_group_ids = [ + endpoint_group.id for endpoint_group in endpoint_groups_resp + ] + self.assertEqual(expected_endpoint_group_ids, + actual_endpoint_group_ids) + + def test_list_endpoint_groups_for_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.list_endpoint_groups_for_project, + project=value) + + def test_add_endpoint_group_to_project(self): + endpoint_group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('PUT', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects', project_id], + status_code=201) + + self.manager.add_endpoint_group_to_project( + project=project_id, endpoint_group=endpoint_group_id) + + def test_add_endpoint_group_to_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.add_endpoint_group_to_project, + project=value, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.add_endpoint_group_to_project, + project=uuid.uuid4().hex, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.add_endpoint_group_to_project, + project=value, + endpoint_group=uuid.uuid4().hex) + + def test_check_endpoint_group_in_project(self): + endpoint_group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('HEAD', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects', project_id], + status_code=201) + + self.manager.check_endpoint_group_in_project( + project=project_id, endpoint_group=endpoint_group_id) + + def test_check_endpoint_group_in_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.check_endpoint_group_in_project, + project=value, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.check_endpoint_group_in_project, + project=uuid.uuid4().hex, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.check_endpoint_group_in_project, + project=value, + endpoint_group=uuid.uuid4().hex) + + def test_delete_endpoint_group_from_project(self): + endpoint_group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('DELETE', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects', project_id], + status_code=201) + + self.manager.delete_endpoint_group_from_project( + project=project_id, endpoint_group=endpoint_group_id) + + def test_delete_endpoint_group_from_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.delete_endpoint_group_from_project, + project=value, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.delete_endpoint_group_from_project, + project=uuid.uuid4().hex, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.delete_endpoint_group_from_project, + project=value, + endpoint_group=uuid.uuid4().hex) diff --git a/keystoneclient/tests/unit/v3/test_endpoint_groups.py b/keystoneclient/tests/unit/v3/test_endpoint_groups.py new file mode 100644 index 000000000..364fd53c2 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_endpoint_groups.py @@ -0,0 +1,34 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import endpoint_groups + + +class EndpointGroupTests(utils.ClientTestCase, utils.CrudTests): + + def setUp(self): + super(EndpointGroupTests, self).setUp() + self.key = 'endpoint_group' + self.collection_key = 'endpoint_groups' + self.model = endpoint_groups.EndpointGroup + self.manager = self.client.endpoint_groups + self.path_prefix = 'OS-EP-FILTER' + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('filters', '{"interface": "public"}') + kwargs.setdefault('description', uuid.uuid4().hex) + return kwargs diff --git a/keystoneclient/tests/unit/v3/test_endpoint_policy.py b/keystoneclient/tests/unit/v3/test_endpoint_policy.py new file mode 100644 index 000000000..69d90ae5a --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_endpoint_policy.py @@ -0,0 +1,242 @@ +# Copyright 2014 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v3 import test_endpoint_filter +from keystoneclient.tests.unit.v3 import utils + + +class EndpointPolicyTests(utils.ClientTestCase, + test_endpoint_filter.EndpointTestUtils): + """Test policy-endpoint associations (a.k.a. EndpointPolicy Extension).""" + + def setUp(self): + super(EndpointPolicyTests, self).setUp() + self.manager = self.client.endpoint_policy + + def new_policy_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('blob', uuid.uuid4().hex) + return kwargs + + def new_region_ref(self, **kwargs): + kwargs = self.new_ref(**kwargs) + return kwargs + + def new_service_ref(self, **kwargs): + kwargs = self.new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + return kwargs + + def _crud_policy_association_for_endpoint_via_id( + self, http_action, manager_action): + policy_id = uuid.uuid4().hex + endpoint_id = uuid.uuid4().hex + + self.stub_url(http_action, + ['policies', policy_id, self.manager.OS_EP_POLICY_EXT, + 'endpoints', endpoint_id], + status_code=204) + manager_action(policy=policy_id, endpoint=endpoint_id) + + def _crud_policy_association_for_endpoint_via_obj( + self, http_action, manager_action): + policy_ref = self.new_policy_ref() + endpoint_ref = self.new_endpoint_ref() + policy = self.client.policies.resource_class( + self.client.policies, policy_ref, loaded=True) + endpoint = self.client.endpoints.resource_class( + self.client.endpoints, endpoint_ref, loaded=True) + + self.stub_url(http_action, + ['policies', policy_ref['id'], + self.manager.OS_EP_POLICY_EXT, + 'endpoints', endpoint_ref['id']], + status_code=204) + manager_action(policy=policy, endpoint=endpoint) + + def test_create_policy_association_for_endpoint_via_id(self): + self._crud_policy_association_for_endpoint_via_id( + 'PUT', self.manager.create_policy_association_for_endpoint) + + def test_create_policy_association_for_endpoint_via_obj(self): + self._crud_policy_association_for_endpoint_via_obj( + 'PUT', self.manager.create_policy_association_for_endpoint) + + def test_check_policy_association_for_endpoint_via_id(self): + self._crud_policy_association_for_endpoint_via_id( + 'HEAD', self.manager.check_policy_association_for_endpoint) + + def test_check_policy_association_for_endpoint_via_obj(self): + self._crud_policy_association_for_endpoint_via_obj( + 'HEAD', self.manager.check_policy_association_for_endpoint) + + def test_delete_policy_association_for_endpoint_via_id(self): + self._crud_policy_association_for_endpoint_via_id( + 'DELETE', self.manager.delete_policy_association_for_endpoint) + + def test_delete_policy_association_for_endpoint_via_obj(self): + self._crud_policy_association_for_endpoint_via_obj( + 'DELETE', self.manager.delete_policy_association_for_endpoint) + + def _crud_policy_association_for_service_via_id( + self, http_action, manager_action): + policy_id = uuid.uuid4().hex + service_id = uuid.uuid4().hex + + self.stub_url(http_action, + ['policies', policy_id, self.manager.OS_EP_POLICY_EXT, + 'services', service_id], + status_code=204) + manager_action(policy=policy_id, service=service_id) + + def _crud_policy_association_for_service_via_obj( + self, http_action, manager_action): + policy_ref = self.new_policy_ref() + service_ref = self.new_service_ref() + policy = self.client.policies.resource_class( + self.client.policies, policy_ref, loaded=True) + service = self.client.services.resource_class( + self.client.services, service_ref, loaded=True) + + self.stub_url(http_action, + ['policies', policy_ref['id'], + self.manager.OS_EP_POLICY_EXT, + 'services', service_ref['id']], + status_code=204) + manager_action(policy=policy, service=service) + + def test_create_policy_association_for_service_via_id(self): + self._crud_policy_association_for_service_via_id( + 'PUT', self.manager.create_policy_association_for_service) + + def test_create_policy_association_for_service_via_obj(self): + self._crud_policy_association_for_service_via_obj( + 'PUT', self.manager.create_policy_association_for_service) + + def test_check_policy_association_for_service_via_id(self): + self._crud_policy_association_for_service_via_id( + 'HEAD', self.manager.check_policy_association_for_service) + + def test_check_policy_association_for_service_via_obj(self): + self._crud_policy_association_for_service_via_obj( + 'HEAD', self.manager.check_policy_association_for_service) + + def test_delete_policy_association_for_service_via_id(self): + self._crud_policy_association_for_service_via_id( + 'DELETE', self.manager.delete_policy_association_for_service) + + def test_delete_policy_association_for_service_via_obj(self): + self._crud_policy_association_for_service_via_obj( + 'DELETE', self.manager.delete_policy_association_for_service) + + def _crud_policy_association_for_region_and_service_via_id( + self, http_action, manager_action): + policy_id = uuid.uuid4().hex + region_id = uuid.uuid4().hex + service_id = uuid.uuid4().hex + + self.stub_url(http_action, + ['policies', policy_id, self.manager.OS_EP_POLICY_EXT, + 'services', service_id, 'regions', region_id], + status_code=204) + manager_action(policy=policy_id, region=region_id, service=service_id) + + def _crud_policy_association_for_region_and_service_via_obj( + self, http_action, manager_action): + policy_ref = self.new_policy_ref() + region_ref = self.new_region_ref() + service_ref = self.new_service_ref() + policy = self.client.policies.resource_class( + self.client.policies, policy_ref, loaded=True) + region = self.client.regions.resource_class( + self.client.regions, region_ref, loaded=True) + service = self.client.services.resource_class( + self.client.services, service_ref, loaded=True) + + self.stub_url(http_action, + ['policies', policy_ref['id'], + self.manager.OS_EP_POLICY_EXT, + 'services', service_ref['id'], + 'regions', region_ref['id']], + status_code=204) + manager_action(policy=policy, region=region, service=service) + + def test_create_policy_association_for_region_and_service_via_id(self): + self._crud_policy_association_for_region_and_service_via_id( + 'PUT', + self.manager.create_policy_association_for_region_and_service) + + def test_create_policy_association_for_region_and_service_via_obj(self): + self._crud_policy_association_for_region_and_service_via_obj( + 'PUT', + self.manager.create_policy_association_for_region_and_service) + + def test_check_policy_association_for_region_and_service_via_id(self): + self._crud_policy_association_for_region_and_service_via_id( + 'HEAD', + self.manager.check_policy_association_for_region_and_service) + + def test_check_policy_association_for_region_and_service_via_obj(self): + self._crud_policy_association_for_region_and_service_via_obj( + 'HEAD', + self.manager.check_policy_association_for_region_and_service) + + def test_delete_policy_association_for_region_and_service_via_id(self): + self._crud_policy_association_for_region_and_service_via_id( + 'DELETE', + self.manager.delete_policy_association_for_region_and_service) + + def test_delete_policy_association_for_region_and_service_via_obj(self): + self._crud_policy_association_for_region_and_service_via_obj( + 'DELETE', + self.manager.delete_policy_association_for_region_and_service) + + def test_get_policy_for_endpoint(self): + endpoint_id = uuid.uuid4().hex + expected_policy = self.new_policy_ref() + + self.stub_url('GET', + ['endpoints', endpoint_id, self.manager.OS_EP_POLICY_EXT, + 'policy'], + json={'policy': expected_policy}, + status_code=200) + + policy_resp = self.manager.get_policy_for_endpoint( + endpoint=endpoint_id) + + self.assertEqual(expected_policy['id'], policy_resp.id) + self.assertEqual(expected_policy['blob'], policy_resp.blob) + self.assertEqual(expected_policy['type'], policy_resp.type) + + def test_list_endpoints_for_policy(self): + policy_id = uuid.uuid4().hex + endpoints = {'endpoints': [self.new_endpoint_ref(), + self.new_endpoint_ref()]} + self.stub_url('GET', + ['policies', policy_id, self.manager.OS_EP_POLICY_EXT, + 'endpoints'], + json=endpoints, + status_code=200) + + endpoints_resp = self.manager.list_endpoints_for_policy( + policy=policy_id) + + expected_endpoint_ids = [ + endpoint['id'] for endpoint in endpoints['endpoints']] + actual_endpoint_ids = [endpoint.id for endpoint in endpoints_resp] + self.assertEqual(expected_endpoint_ids, actual_endpoint_ids) diff --git a/tests/v3/test_endpoints.py b/keystoneclient/tests/unit/v3/test_endpoints.py similarity index 60% rename from tests/v3/test_endpoints.py rename to keystoneclient/tests/unit/v3/test_endpoints.py index 29bf226da..40fc03be8 100644 --- a/tests/v3/test_endpoints.py +++ b/keystoneclient/tests/unit/v3/test_endpoints.py @@ -1,14 +1,25 @@ -import testtools +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import uuid +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils from keystoneclient.v3 import endpoints -from tests.v3 import utils -class EndpointTests(utils.TestCase, utils.CrudTests): +class EndpointTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(EndpointTests, self).setUp() - self.additionalSetUp() self.key = 'endpoint' self.collection_key = 'endpoints' self.model = endpoints.Endpoint @@ -37,7 +48,7 @@ def test_create_internal_interface(self): def test_create_invalid_interface(self): ref = self.new_ref(interface=uuid.uuid4().hex) - self.assertRaises(Exception, self.manager.create, + self.assertRaises(exceptions.ValidationError, self.manager.create, **utils.parameterize(ref)) def test_update_public_interface(self): @@ -54,7 +65,8 @@ def test_update_internal_interface(self): def test_update_invalid_interface(self): ref = self.new_ref(interface=uuid.uuid4().hex) - self.assertRaises(Exception, self.manager.update, + ref['endpoint'] = "fake_endpoint" + self.assertRaises(exceptions.ValidationError, self.manager.update, **utils.parameterize(ref)) def test_list_public_interface(self): @@ -75,5 +87,18 @@ def test_list_internal_interface(self): def test_list_invalid_interface(self): interface = uuid.uuid4().hex expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) - self.assertRaises(Exception, self.manager.list, + self.assertRaises(exceptions.ValidationError, self.manager.list, expected_path=expected_path, interface=interface) + + def test_list_filtered_by_region(self): + region_id = uuid.uuid4().hex + ref_list = [self.new_ref(region=region_id), + self.new_ref(region=region_id)] + expected_path = 'v3/%s?region_id=%s' % (self.collection_key, region_id) + expected_query = {'region_id': region_id} + + # Validate passing either region or region_id result to the API call. + self.test_list(ref_list=ref_list, expected_path=expected_path, + expected_query=expected_query, region=region_id) + self.test_list(ref_list=ref_list, expected_path=expected_path, + expected_query=expected_query, region_id=region_id) diff --git a/keystoneclient/tests/unit/v3/test_federation.py b/keystoneclient/tests/unit/v3/test_federation.py new file mode 100644 index 000000000..08391c7c1 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_federation.py @@ -0,0 +1,825 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import fixtures +import uuid + +from keystoneauth1 import exceptions +from keystoneauth1 import fixture +from keystoneauth1.identity import v3 +from keystoneauth1 import session +from keystoneauth1.tests.unit import k2k_fixtures +from testtools import matchers + +from keystoneclient import access +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import client +from keystoneclient.v3.contrib.federation import base +from keystoneclient.v3.contrib.federation import identity_providers +from keystoneclient.v3.contrib.federation import mappings +from keystoneclient.v3.contrib.federation import protocols +from keystoneclient.v3.contrib.federation import service_providers +from keystoneclient.v3 import domains +from keystoneclient.v3 import projects + + +class IdentityProviderTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(IdentityProviderTests, self).setUp() + self.key = 'identity_provider' + self.collection_key = 'identity_providers' + self.model = identity_providers.IdentityProvider + self.manager = self.client.federation.identity_providers + self.path_prefix = 'OS-FEDERATION' + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs + + def test_positional_parameters_expect_fail(self): + """Ensure CrudManager raises TypeError exceptions. + + After passing wrong number of positional arguments + an exception should be raised. + + Operations to be tested: + * create() + * get() + * list() + * delete() + * update() + + """ + POS_PARAM_1 = uuid.uuid4().hex + POS_PARAM_2 = uuid.uuid4().hex + POS_PARAM_3 = uuid.uuid4().hex + + PARAMETERS = { + 'create': (POS_PARAM_1, POS_PARAM_2), + 'get': (POS_PARAM_1, POS_PARAM_2), + 'list': (POS_PARAM_1, POS_PARAM_2), + 'update': (POS_PARAM_1, POS_PARAM_2, POS_PARAM_3), + 'delete': (POS_PARAM_1, POS_PARAM_2) + } + + for f_name, args in PARAMETERS.items(): + self.assertRaises(TypeError, getattr(self.manager, f_name), + *args) + + def test_create(self): + ref = self.new_ref() + req_ref = ref.copy() + req_ref.pop('id') + + self.stub_entity('PUT', entity=ref, id=ref['id'], status_code=201) + + returned = self.manager.create(**ref) + self.assertIsInstance(returned, self.model) + for attr in req_ref: + self.assertEqual( + getattr(returned, attr), + req_ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + +class MappingTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(MappingTests, self).setUp() + self.key = 'mapping' + self.collection_key = 'mappings' + self.model = mappings.Mapping + self.manager = self.client.federation.mappings + self.path_prefix = 'OS-FEDERATION' + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('rules', [uuid.uuid4().hex, + uuid.uuid4().hex]) + return kwargs + + def test_create(self): + ref = self.new_ref() + manager_ref = ref.copy() + mapping_id = manager_ref.pop('id') + req_ref = ref.copy() + + self.stub_entity('PUT', entity=req_ref, id=mapping_id, + status_code=201) + + returned = self.manager.create(mapping_id=mapping_id, **manager_ref) + self.assertIsInstance(returned, self.model) + for attr in req_ref: + self.assertEqual( + getattr(returned, attr), + req_ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(manager_ref) + + +class ProtocolTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(ProtocolTests, self).setUp() + self.key = 'protocol' + self.collection_key = 'protocols' + self.model = protocols.Protocol + self.manager = self.client.federation.protocols + self.path_prefix = 'OS-FEDERATION/identity_providers' + + def _transform_to_response(self, ref): + """Construct a response body from a dictionary.""" + response = copy.deepcopy(ref) + del response['identity_provider'] + return response + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('mapping_id', uuid.uuid4().hex) + kwargs.setdefault('identity_provider', uuid.uuid4().hex) + return kwargs + + def build_parts(self, idp_id, protocol_id=None): + """Build array used to construct mocking URL. + + Construct and return array with URL parts later used + by methods like utils.TestCase.stub_entity(). + Example of URL: + ``OS-FEDERATION/identity_providers/{idp_id}/ + protocols/{protocol_id}`` + + """ + parts = ['OS-FEDERATION', 'identity_providers', + idp_id, 'protocols'] + if protocol_id: + parts.append(protocol_id) + return parts + + def test_build_url_provide_base_url(self): + base_url = uuid.uuid4().hex + parameters = {'base_url': base_url} + url = self.manager.build_url(dict_args_in_out=parameters) + self.assertEqual('/'.join([base_url, self.collection_key]), url) + + def test_build_url_w_idp_id(self): + """Test whether kwargs ``base_url`` discards object's base_url. + + This test shows, that when ``base_url`` is specified in the + dict_args_in_out dictionary, values like ``identity_provider_id`` + are not taken into consideration while building the url. + + """ + base_url, identity_provider_id = uuid.uuid4().hex, uuid.uuid4().hex + parameters = { + 'base_url': base_url, + 'identity_provider_id': identity_provider_id + } + url = self.manager.build_url(dict_args_in_out=parameters) + self.assertEqual('/'.join([base_url, self.collection_key]), url) + + def test_build_url_default_base_url(self): + identity_provider_id = uuid.uuid4().hex + parameters = { + 'identity_provider_id': identity_provider_id + } + + url = self.manager.build_url(dict_args_in_out=parameters) + self.assertEqual( + '/'.join([self.manager.base_url, identity_provider_id, + self.manager.collection_key]), url) + + def test_create(self): + """Test creating federation protocol tied to an Identity Provider. + + URL to be tested: PUT /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + """ + ref = self.new_ref() + expected = self._transform_to_response(ref) + parts = self.build_parts( + idp_id=ref['identity_provider'], + protocol_id=ref['id']) + self.stub_entity('PUT', entity=expected, + parts=parts, status_code=201) + returned = self.manager.create( + protocol_id=ref['id'], + identity_provider=ref['identity_provider'], + mapping=ref['mapping_id']) + self.assertEqual(expected, returned.to_dict()) + request_body = {'mapping_id': ref['mapping_id']} + self.assertEntityRequestBodyIs(request_body) + + def test_get(self): + """Fetch federation protocol object. + + URL to be tested: GET /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + """ + ref = self.new_ref() + expected = self._transform_to_response(ref) + + parts = self.build_parts( + idp_id=ref['identity_provider'], + protocol_id=ref['id']) + self.stub_entity('GET', entity=expected, + parts=parts, status_code=201) + + returned = self.manager.get(ref['identity_provider'], + ref['id']) + self.assertIsInstance(returned, self.model) + self.assertEqual(expected, returned.to_dict()) + + def test_delete(self): + """Delete federation protocol object. + + URL to be tested: DELETE /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + """ + ref = self.new_ref() + parts = self.build_parts( + idp_id=ref['identity_provider'], + protocol_id=ref['id']) + + self.stub_entity('DELETE', parts=parts, status_code=204) + + self.manager.delete(ref['identity_provider'], + ref['id']) + + def test_list(self): + """Test listing all federation protocols tied to the Identity Provider. + + URL to be tested: GET /OS-FEDERATION/identity_providers/ + $identity_provider/protocols + + """ + def _ref_protocols(): + return { + 'id': uuid.uuid4().hex, + 'mapping_id': uuid.uuid4().hex + } + + ref = self.new_ref() + expected = [_ref_protocols() for _ in range(3)] + parts = self.build_parts(idp_id=ref['identity_provider']) + self.stub_entity('GET', parts=parts, + entity=expected, status_code=200) + + returned = self.manager.list(ref['identity_provider']) + for obj, ref_obj in zip(returned, expected): + self.assertEqual(obj.to_dict(), ref_obj) + + def test_list_by_id(self): + # The test in the parent class needs to be overridden because it + # assumes globally unique IDs, which is not the case with protocol IDs + # (which are contextualized per identity provider). + ref = self.new_ref() + super(ProtocolTests, self).test_list_by_id( + ref=ref, + identity_provider=ref['identity_provider'], + id=ref['id']) + + def test_list_params(self): + request_args = self.new_ref() + filter_kwargs = {uuid.uuid4().hex: uuid.uuid4().hex} + parts = self.build_parts(request_args['identity_provider']) + + # Return HTTP 401 as we don't accept such requests. + self.stub_entity('GET', parts=parts, status_code=401) + self.assertRaises(exceptions.Unauthorized, + self.manager.list, + request_args['identity_provider'], + **filter_kwargs) + self.assertQueryStringContains(**filter_kwargs) + + def test_update(self): + """Test updating federation protocol. + + URL to be tested: PATCH /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + """ + ref = self.new_ref() + expected = self._transform_to_response(ref) + + parts = self.build_parts( + idp_id=ref['identity_provider'], + protocol_id=ref['id']) + + self.stub_entity('PATCH', parts=parts, + entity=expected, status_code=200) + + returned = self.manager.update(ref['identity_provider'], + ref['id'], + mapping=ref['mapping_id']) + self.assertIsInstance(returned, self.model) + self.assertEqual(expected, returned.to_dict()) + request_body = {'mapping_id': ref['mapping_id']} + self.assertEntityRequestBodyIs(request_body) + + +class EntityManagerTests(utils.ClientTestCase): + def test_create_object_expect_fail(self): + self.assertRaises(TypeError, + base.EntityManager, + self.client) + + +class FederationProjectTests(utils.ClientTestCase): + + def setUp(self): + super(FederationProjectTests, self).setUp() + self.key = 'project' + self.collection_key = 'projects' + self.model = projects.Project + self.manager = self.client.federation.projects + self.URL = "%s%s" % (self.TEST_URL, '/auth/projects') + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_list_accessible_projects(self): + projects_ref = [self.new_ref(), self.new_ref()] + projects_json = { + self.collection_key: [self.new_ref(), self.new_ref()] + } + self.requests_mock.get(self.URL, json=projects_json) + returned_list = self.manager.list() + + self.assertEqual(len(projects_ref), len(returned_list)) + for project in returned_list: + self.assertIsInstance(project, self.model) + + +class K2KFederatedProjectTests(utils.TestCase): + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + TEST_PASS = 'password' + REQUEST_ECP_URL = TEST_URL + '/auth/OS-FEDERATION/saml2/ecp' + + SP_ID = 'sp1' + SP_ROOT_URL = 'https://example.com/v3' + SP_URL = 'https://example.com/Shibboleth.sso/SAML2/ECP' + SP_AUTH_URL = (SP_ROOT_URL + + '/OS-FEDERATION/identity_providers' + '/testidp/protocols/saml2/auth') + + def setUp(self): + super(K2KFederatedProjectTests, self).setUp() + self.token_v3 = fixture.V3Token() + self.token_v3.add_service_provider( + self.SP_ID, self.SP_AUTH_URL, self.SP_URL) + self.session = session.Session() + self.collection_key = 'projects' + self.model = projects.Project + self.URL = '%s%s' % (self.SP_ROOT_URL, '/auth/projects') + self.k2kplugin = self.get_plugin() + self._mock_k2k_flow_urls() + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def _get_base_plugin(self): + self.stub_url('POST', ['auth', 'tokens'], + headers={'X-Subject-Token': uuid.uuid4().hex}, + json=self.token_v3) + return v3.Password(self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_PASS) + + def _mock_k2k_flow_urls(self): + # We need to check the auth versions available + self.requests_mock.get( + self.TEST_URL, + json={'version': fixture.V3Discovery(self.TEST_URL)}, + headers={'Content-Type': 'application/json'}) + + # The identity provider receives a request for an ECP wrapped + # assertion. This assertion contains the user authentication info + # and will be presented to the service provider + self.requests_mock.register_uri( + 'POST', + self.REQUEST_ECP_URL, + content=k2k_fixtures.ECP_ENVELOPE.encode(), + headers={'Content-Type': 'application/vnd.paos+xml'}, + status_code=200) + + # The service provider is presented with the ECP wrapped assertion + # generated by the identity provider and should return a redirect + # (302 or 303) upon successful authentication + self.requests_mock.register_uri( + 'POST', + self.SP_URL, + content=k2k_fixtures.TOKEN_BASED_ECP.encode(), + headers={'Content-Type': 'application/vnd.paos+xml'}, + status_code=302) + + # Should not follow the redirect URL, but use the auth_url attribute + self.requests_mock.register_uri( + 'GET', + self.SP_AUTH_URL, + json=k2k_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': k2k_fixtures.UNSCOPED_TOKEN_HEADER}) + + def get_plugin(self, **kwargs): + kwargs.setdefault('base_plugin', self._get_base_plugin()) + kwargs.setdefault('service_provider', self.SP_ID) + return v3.Keystone2Keystone(**kwargs) + + def test_list_projects(self): + k2k_client = client.Client(session=self.session, auth=self.k2kplugin) + self.requests_mock.get(self.URL, json={ + self.collection_key: [self.new_ref(), self.new_ref()] + }) + self.requests_mock.get(self.SP_ROOT_URL, json={ + 'version': fixture.discovery.V3Discovery(self.SP_ROOT_URL) + }) + returned_list = k2k_client.federation.projects.list() + + self.assertThat(returned_list, matchers.HasLength(2)) + for project in returned_list: + self.assertIsInstance(project, self.model) + + +class FederationDomainTests(utils.ClientTestCase): + + def setUp(self): + super(FederationDomainTests, self).setUp() + self.key = 'domain' + self.collection_key = 'domains' + self.model = domains.Domain + self.manager = self.client.federation.domains + + self.URL = "%s%s" % (self.TEST_URL, '/auth/domains') + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + return kwargs + + def test_list_accessible_domains(self): + domains_ref = [self.new_ref(), self.new_ref()] + domains_json = { + self.collection_key: domains_ref + } + self.requests_mock.get(self.URL, json=domains_json) + returned_list = self.manager.list() + self.assertEqual(len(domains_ref), len(returned_list)) + for domain in returned_list: + self.assertIsInstance(domain, self.model) + + +class FederatedTokenTests(utils.ClientTestCase): + + def setUp(self): + super(FederatedTokenTests, self).setUp() + token = fixture.V3FederationToken() + token.set_project_scope() + token.add_role() + self.federated_token = access.AccessInfo.factory(body=token) + + def test_federated_property_federated_token(self): + """Check if is_federated property returns expected value.""" + self.assertTrue(self.federated_token.is_federated) + + def test_get_user_domain_name(self): + """Ensure a federated user's domain name does not exist.""" + self.assertIsNone(self.federated_token.user_domain_name) + + def test_get_user_domain_id(self): + """Ensure a federated user's domain ID does not exist.""" + self.assertEqual('Federated', self.federated_token.user_domain_id) + + +class ServiceProviderTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(ServiceProviderTests, self).setUp() + self.key = 'service_provider' + self.collection_key = 'service_providers' + self.model = service_providers.ServiceProvider + self.manager = self.client.federation.service_providers + self.path_prefix = 'OS-FEDERATION' + + def new_ref(self, **kwargs): + kwargs.setdefault('auth_url', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('sp_url', uuid.uuid4().hex) + return kwargs + + def test_positional_parameters_expect_fail(self): + """Ensure CrudManager raises TypeError exceptions. + + After passing wrong number of positional arguments + an exception should be raised. + + Operations to be tested: + * create() + * get() + * list() + * delete() + * update() + + """ + POS_PARAM_1 = uuid.uuid4().hex + POS_PARAM_2 = uuid.uuid4().hex + POS_PARAM_3 = uuid.uuid4().hex + + PARAMETERS = { + 'create': (POS_PARAM_1, POS_PARAM_2), + 'get': (POS_PARAM_1, POS_PARAM_2), + 'list': (POS_PARAM_1, POS_PARAM_2), + 'update': (POS_PARAM_1, POS_PARAM_2, POS_PARAM_3), + 'delete': (POS_PARAM_1, POS_PARAM_2) + } + + for f_name, args in PARAMETERS.items(): + self.assertRaises(TypeError, getattr(self.manager, f_name), + *args) + + def test_create(self): + ref = self.new_ref() + + # req_ref argument allows you to specify a different + # signature for the request when the manager does some + # conversion before doing the request (e.g. converting + # from datetime object to timestamp string) + req_ref = ref.copy() + req_ref.pop('id') + + self.stub_entity('PUT', entity=ref, id=ref['id'], status_code=201) + + returned = self.manager.create(**ref) + self.assertIsInstance(returned, self.model) + for attr in req_ref: + self.assertEqual( + getattr(returned, attr), + req_ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + +class IdentityProviderRequestIdTests(utils.TestRequestId): + + def setUp(self): + super(IdentityProviderRequestIdTests, self).setUp() + self.mgr = identity_providers.IdentityProviderManager(self.client) + + def _mock_request_method(self, method=None, body=None): + return self.useFixture(fixtures.MockPatchObject( + self.client, method, autospec=True, + return_value=(self.resp, body)) + ).mock + + def test_get_identity_provider(self): + body = {"identity_provider": {"name": "admin"}} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.get("admin") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with( + 'OS-FEDERATION/identity_providers/admin') + + def test_list_identity_provider(self): + body = {"identity_providers": [{"name": "admin"}]} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.list() + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with('OS-FEDERATION/identity_providers?') + + def test_create_identity_provider(self): + body = {"identity_provider": {"name": "admin"}} + self._mock_request_method(method='post', body=body) + put_mock = self._mock_request_method(method='put', body=body) + + response = self.mgr.create(id="admin", description='fake') + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + put_mock.assert_called_once_with( + 'OS-FEDERATION/identity_providers/admin', + body={'identity_provider': {'description': 'fake'}}) + + def test_update_identity_provider(self): + body = {"identity_provider": {"name": "admin"}} + patch_mock = self._mock_request_method(method='patch', body=body) + self._mock_request_method(method='post', body=body) + + response = self.mgr.update("admin") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + patch_mock.assert_called_once_with( + 'OS-FEDERATION/identity_providers/admin', body={ + 'identity_provider': {}}) + + def test_delete_identity_provider(self): + get_mock = self._mock_request_method(method='delete') + + _, resp = self.mgr.delete("admin") + self.assertEqual(resp.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with( + 'OS-FEDERATION/identity_providers/admin') + + +class MappingRequestIdTests(utils.TestRequestId): + + def setUp(self): + super(MappingRequestIdTests, self).setUp() + self.mgr = mappings.MappingManager(self.client) + + def _mock_request_method(self, method=None, body=None): + return self.useFixture(fixtures.MockPatchObject( + self.client, method, autospec=True, + return_value=(self.resp, body)) + ).mock + + def test_get_mapping(self): + body = {"mapping": {"name": "admin"}} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.get("admin") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with('OS-FEDERATION/mappings/admin') + + def test_list_mapping(self): + body = {"mappings": [{"name": "admin"}]} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.list() + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with('OS-FEDERATION/mappings?') + + def test_create_mapping(self): + body = {"mapping": {"name": "admin"}} + self._mock_request_method(method='post', body=body) + put_mock = self._mock_request_method(method='put', body=body) + + response = self.mgr.create(mapping_id="admin", description='fake') + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + put_mock.assert_called_once_with( + 'OS-FEDERATION/mappings/admin', body={ + 'mapping': {'description': 'fake'}}) + + def test_update_mapping(self): + body = {"mapping": {"name": "admin"}} + patch_mock = self._mock_request_method(method='patch', body=body) + self._mock_request_method(method='post', body=body) + + response = self.mgr.update("admin") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + patch_mock.assert_called_once_with( + 'OS-FEDERATION/mappings/admin', body={'mapping': {}}) + + def test_delete_mapping(self): + get_mock = self._mock_request_method(method='delete') + + _, resp = self.mgr.delete("admin") + self.assertEqual(resp.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with('OS-FEDERATION/mappings/admin') + + +class ProtocolRequestIdTests(utils.TestRequestId): + + def setUp(self): + super(ProtocolRequestIdTests, self).setUp() + self.mgr = protocols.ProtocolManager(self.client) + + def _mock_request_method(self, method=None, body=None): + return self.useFixture(fixtures.MockPatchObject( + self.client, method, autospec=True, + return_value=(self.resp, body)) + ).mock + + def test_get_protocol(self): + body = {"protocol": {"name": "admin"}} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.get("admin", "protocol") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with( + 'OS-FEDERATION/identity_providers/admin/protocols/protocol') + + def test_list_protocol(self): + body = {"protocols": [{"name": "admin"}]} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.list("identity_provider") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with( + 'OS-FEDERATION/identity_providers/identity_provider/protocols?') + + def test_create_protocol(self): + body = {"protocol": {"name": "admin"}} + self._mock_request_method(method='post', body=body) + put_mock = self._mock_request_method(method='put', body=body) + + response = self.mgr.create( + protocol_id="admin", identity_provider='fake', mapping='fake') + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + put_mock.assert_called_once_with( + 'OS-FEDERATION/identity_providers/fake/protocols/admin', body={ + 'protocol': {'mapping_id': 'fake'}}) + + def test_update_protocol(self): + body = {"protocol": {"name": "admin"}} + patch_mock = self._mock_request_method(method='patch', body=body) + self._mock_request_method(method='post', body=body) + + response = self.mgr.update(protocol="admin", identity_provider='fake', + mapping='fake') + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + patch_mock.assert_called_once_with( + 'OS-FEDERATION/identity_providers/fake/protocols/admin', body={ + 'protocol': {'mapping_id': 'fake'}}) + + def test_delete_protocol(self): + get_mock = self._mock_request_method(method='delete') + + _, resp = self.mgr.delete("identity_provider", "protocol") + self.assertEqual(resp.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with( + 'OS-FEDERATION/identity_providers/' + 'identity_provider/protocols/protocol') + + +class ServiceProviderRequestIdTests(utils.TestRequestId): + + def setUp(self): + super(ServiceProviderRequestIdTests, self).setUp() + self.mgr = service_providers.ServiceProviderManager(self.client) + + def _mock_request_method(self, method=None, body=None): + return self.useFixture(fixtures.MockPatchObject( + self.client, method, autospec=True, + return_value=(self.resp, body)) + ).mock + + def test_get_service_provider(self): + body = {"service_provider": {"name": "admin"}} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.get("provider") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with( + 'OS-FEDERATION/service_providers/provider') + + def test_list_service_provider(self): + body = {"service_providers": [{"name": "admin"}]} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.list() + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with('OS-FEDERATION/service_providers?') + + def test_create_service_provider(self): + body = {"service_provider": {"name": "admin"}} + self._mock_request_method(method='post', body=body) + put_mock = self._mock_request_method(method='put', body=body) + + response = self.mgr.create(id='provider') + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + put_mock.assert_called_once_with( + 'OS-FEDERATION/service_providers/provider', body={ + 'service_provider': {}}) + + def test_update_service_provider(self): + body = {"service_provider": {"name": "admin"}} + patch_mock = self._mock_request_method(method='patch', body=body) + self._mock_request_method(method='post', body=body) + + response = self.mgr.update("provider") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + patch_mock.assert_called_once_with( + 'OS-FEDERATION/service_providers/provider', body={ + 'service_provider': {}}) + + def test_delete_service_provider(self): + get_mock = self._mock_request_method(method='delete') + + _, resp = self.mgr.delete("provider") + self.assertEqual(resp.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with( + 'OS-FEDERATION/service_providers/provider') diff --git a/tests/v3/test_groups.py b/keystoneclient/tests/unit/v3/test_groups.py similarity index 54% rename from tests/v3/test_groups.py rename to keystoneclient/tests/unit/v3/test_groups.py index b49f911d9..90fff0e63 100644 --- a/tests/v3/test_groups.py +++ b/keystoneclient/tests/unit/v3/test_groups.py @@ -1,6 +1,4 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC +# Copyright 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -14,20 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. -import copy -import urlparse import uuid -import requests - +from keystoneclient.tests.unit.v3 import utils from keystoneclient.v3 import groups -from tests.v3 import utils -class GroupTests(utils.TestCase, utils.CrudTests): +class GroupTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(GroupTests, self).setUp() - self.additionalSetUp() self.key = 'group' self.collection_key = 'groups' self.model = groups.Group @@ -41,23 +34,25 @@ def new_ref(self, **kwargs): def test_list_groups_for_user(self): user_id = uuid.uuid4().hex ref_list = [self.new_ref(), self.new_ref()] - resp = utils.TestResponse({ - "status_code": 200, - "text": self.serialize(ref_list), - }) - - method = 'GET' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/users/%s/%s' % ( - user_id, self.collection_key)), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() + + self.stub_entity('GET', + ['users', user_id, self.collection_key], + status_code=200, entity=ref_list) returned_list = self.manager.list(user=user_id) - self.assertTrue(len(returned_list)) - [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_list_groups_for_domain(self): + ref_list = [self.new_ref(), self.new_ref()] + domain_id = uuid.uuid4().hex + + self.stub_entity('GET', + [self.collection_key], + status_code=200, entity=ref_list) + + returned_list = self.manager.list(domain=domain_id) + self.assertTrue(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + self.assertQueryStringIs('domain_id=%s' % domain_id) diff --git a/keystoneclient/tests/unit/v3/test_limits.py b/keystoneclient/tests/unit/v3/test_limits.py new file mode 100644 index 000000000..0dca67d9b --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_limits.py @@ -0,0 +1,77 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import limits + + +class LimitTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(LimitTests, self).setUp() + self.key = 'limit' + self.collection_key = 'limits' + self.model = limits.Limit + self.manager = self.client.limits + + def new_ref(self, **kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'project_id': uuid.uuid4().hex, + 'service_id': uuid.uuid4().hex, + 'resource_name': uuid.uuid4().hex, + 'resource_limit': 15, + 'description': uuid.uuid4().hex + } + ref.update(kwargs) + return ref + + def test_create(self): + # This test overrides the generic test case provided by the CrudTests + # class because the limits API supports creating multiple limits in a + # single POST request. As a result, it returns the limits as a list of + # all the created limits from the request. This is different from what + # the base test_create() method assumes about keystone's API. The + # changes here override the base test to closely model how the actual + # limit API behaves. + ref = self.new_ref() + manager_ref = ref.copy() + manager_ref.pop('id') + req_ref = [manager_ref.copy()] + + self.stub_entity('POST', entity=req_ref, status_code=201) + + returned = self.manager.create(**utils.parameterize(manager_ref)) + self.assertIsInstance(returned, self.model) + + expected_limit = req_ref.pop() + for attr in expected_limit: + self.assertEqual( + getattr(returned, attr), + expected_limit[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs([expected_limit]) + + def test_list_filter_by_service(self): + service_id = uuid.uuid4().hex + expected_query = {'service_id': service_id} + self.test_list(expected_query=expected_query, service=service_id) + + def test_list_filtered_by_resource_name(self): + resource_name = uuid.uuid4().hex + self.test_list(resource_name=resource_name) + + def test_list_filtered_by_region(self): + region_id = uuid.uuid4().hex + expected_query = {'region_id': region_id} + self.test_list(expected_query=expected_query, region=region_id) diff --git a/keystoneclient/tests/unit/v3/test_oauth1.py b/keystoneclient/tests/unit/v3/test_oauth1.py new file mode 100644 index 000000000..a15d94b83 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_oauth1.py @@ -0,0 +1,338 @@ +# 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 unittest import mock + +import fixtures +from urllib import parse as urlparse +import uuid + +from testtools import matchers + +from keystoneclient import session +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import utils +from keystoneclient import utils as client_utils +from keystoneclient.v3.contrib.oauth1 import access_tokens +from keystoneclient.v3.contrib.oauth1 import auth +from keystoneclient.v3.contrib.oauth1 import consumers +from keystoneclient.v3.contrib.oauth1 import request_tokens + +try: + from oauthlib import oauth1 +except ImportError: + oauth1 = None + + +class ConsumerTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + if oauth1 is None: + self.skipTest('oauthlib package not available') + + super(ConsumerTests, self).setUp() + self.key = 'consumer' + self.collection_key = 'consumers' + self.model = consumers.Consumer + self.manager = self.client.oauth1.consumers + self.path_prefix = 'OS-OAUTH1' + + def new_ref(self, **kwargs): + kwargs = super(ConsumerTests, self).new_ref(**kwargs) + kwargs.setdefault('description', uuid.uuid4().hex) + return kwargs + + def test_description_is_optional(self): + consumer_id = uuid.uuid4().hex + resp_ref = {'consumer': {'description': None, + 'id': consumer_id}} + + self.stub_url('POST', + [self.path_prefix, self.collection_key], + status_code=201, json=resp_ref) + + consumer = self.manager.create() + self.assertEqual(consumer_id, consumer.id) + self.assertIsNone(consumer.description) + + def test_description_not_included(self): + consumer_id = uuid.uuid4().hex + resp_ref = {'consumer': {'id': consumer_id}} + + self.stub_url('POST', + [self.path_prefix, self.collection_key], + status_code=201, json=resp_ref) + + consumer = self.manager.create() + self.assertEqual(consumer_id, consumer.id) + + +class TokenTests(object): + def _new_oauth_token(self): + key = uuid.uuid4().hex + secret = uuid.uuid4().hex + params = {'oauth_token': key, 'oauth_token_secret': secret} + token = urlparse.urlencode(params) + return (key, secret, token) + + def _new_oauth_token_with_expires_at(self): + key, secret, token = self._new_oauth_token() + expires_at = client_utils.strtime() + params = {'oauth_token': key, + 'oauth_token_secret': secret, + 'oauth_expires_at': expires_at} + token = urlparse.urlencode(params) + return (key, secret, expires_at, token) + + def _validate_oauth_headers(self, auth_header, oauth_client): + """Validate data in the headers. + + Assert that the data in the headers matches the data + that is produced from oauthlib. + """ + self.assertThat(auth_header, matchers.StartsWith('OAuth ')) + parameters = dict( + oauth1.rfc5849.utils.parse_authorization_header(auth_header)) + + self.assertEqual('HMAC-SHA1', parameters['oauth_signature_method']) + self.assertEqual('1.0', parameters['oauth_version']) + self.assertIsInstance(parameters['oauth_nonce'], str) + self.assertEqual(oauth_client.client_key, + parameters['oauth_consumer_key']) + if oauth_client.resource_owner_key: + self.assertEqual(oauth_client.resource_owner_key, + parameters['oauth_token'],) + if oauth_client.verifier: + self.assertEqual(oauth_client.verifier, + parameters['oauth_verifier']) + if oauth_client.callback_uri: + self.assertEqual(oauth_client.callback_uri, + parameters['oauth_callback']) + return parameters + + +class RequestTokenTests(utils.ClientTestCase, TokenTests): + def setUp(self): + if oauth1 is None: + self.skipTest('oauthlib package not available') + + super(RequestTokenTests, self).setUp() + self.model = request_tokens.RequestToken + self.manager = self.client.oauth1.request_tokens + self.path_prefix = 'OS-OAUTH1' + + def test_authorize_request_token(self): + request_key = uuid.uuid4().hex + info = {'id': request_key, + 'key': request_key, + 'secret': uuid.uuid4().hex} + request_token = request_tokens.RequestToken(self.manager, info) + + verifier = uuid.uuid4().hex + resp_ref = {'token': {'oauth_verifier': verifier}} + self.stub_url('PUT', + [self.path_prefix, 'authorize', request_key], + status_code=200, json=resp_ref) + + # Assert the manager is returning the expected data + role_id = uuid.uuid4().hex + token = request_token.authorize([role_id]) + self.assertEqual(verifier, token.oauth_verifier) + + # Assert that the request was sent in the expected structure + exp_body = {'roles': [{'id': role_id}]} + self.assertRequestBodyIs(json=exp_body) + + def test_create_request_token(self): + project_id = uuid.uuid4().hex + consumer_key = uuid.uuid4().hex + consumer_secret = uuid.uuid4().hex + + request_key, request_secret, resp_ref = self._new_oauth_token() + + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + self.stub_url('POST', [self.path_prefix, 'request_token'], + status_code=201, text=resp_ref, headers=headers) + + # Assert the manager is returning request token object + request_token = self.manager.create(consumer_key, consumer_secret, + project_id) + self.assertIsInstance(request_token, self.model) + self.assertEqual(request_key, request_token.key) + self.assertEqual(request_secret, request_token.secret) + + # Assert that the project id is in the header + self.assertRequestHeaderEqual('requested-project-id', project_id) + req_headers = self.requests_mock.last_request.headers + + oauth_client = oauth1.Client(consumer_key, + client_secret=consumer_secret, + signature_method=oauth1.SIGNATURE_HMAC, + callback_uri="oob") + self._validate_oauth_headers(req_headers['Authorization'], + oauth_client) + + +class AccessTokenTests(utils.ClientTestCase, TokenTests): + def setUp(self): + if oauth1 is None: + self.skipTest('oauthlib package not available') + + super(AccessTokenTests, self).setUp() + self.manager = self.client.oauth1.access_tokens + self.model = access_tokens.AccessToken + self.path_prefix = 'OS-OAUTH1' + + def test_create_access_token_expires_at(self): + verifier = uuid.uuid4().hex + consumer_key = uuid.uuid4().hex + consumer_secret = uuid.uuid4().hex + request_key = uuid.uuid4().hex + request_secret = uuid.uuid4().hex + + t = self._new_oauth_token_with_expires_at() + access_key, access_secret, expires_at, resp_ref = t + + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + self.stub_url('POST', [self.path_prefix, 'access_token'], + status_code=201, text=resp_ref, headers=headers) + + # Assert that the manager creates an access token object + access_token = self.manager.create(consumer_key, consumer_secret, + request_key, request_secret, + verifier) + self.assertIsInstance(access_token, self.model) + self.assertEqual(access_key, access_token.key) + self.assertEqual(access_secret, access_token.secret) + self.assertEqual(expires_at, access_token.expires) + + req_headers = self.requests_mock.last_request.headers + oauth_client = oauth1.Client(consumer_key, + client_secret=consumer_secret, + resource_owner_key=request_key, + resource_owner_secret=request_secret, + signature_method=oauth1.SIGNATURE_HMAC, + verifier=verifier) + + self._validate_oauth_headers(req_headers['Authorization'], + oauth_client) + + +class AuthenticateWithOAuthTests(utils.TestCase, TokenTests): + def setUp(self): + super(AuthenticateWithOAuthTests, self).setUp() + if oauth1 is None: + self.skipTest('optional package oauthlib is not installed') + + def test_oauth_authenticate_success(self): + consumer_key = uuid.uuid4().hex + consumer_secret = uuid.uuid4().hex + access_key = uuid.uuid4().hex + access_secret = uuid.uuid4().hex + + # Just use an existing project scoped token and change + # the methods to oauth1, and add an OS-OAUTH1 section. + oauth_token = client_fixtures.project_scoped_token() + oauth_token['methods'] = ["oauth1"] + oauth_token['OS-OAUTH1'] = {"consumer_id": consumer_key, + "access_token_id": access_key} + self.stub_auth(json=oauth_token) + + with self.deprecations.expect_deprecations_here(): + a = auth.OAuth(self.TEST_URL, consumer_key=consumer_key, + consumer_secret=consumer_secret, + access_key=access_key, + access_secret=access_secret) + s = session.Session(auth=a) + t = s.get_token() + self.assertEqual(self.TEST_TOKEN, t) + + OAUTH_REQUEST_BODY = { + "auth": { + "identity": { + "methods": ["oauth1"], + "oauth1": {} + } + } + } + + self.assertRequestBodyIs(json=OAUTH_REQUEST_BODY) + + # Assert that the headers have the same oauthlib data + req_headers = self.requests_mock.last_request.headers + oauth_client = oauth1.Client(consumer_key, + client_secret=consumer_secret, + resource_owner_key=access_key, + resource_owner_secret=access_secret, + signature_method=oauth1.SIGNATURE_HMAC) + self._validate_oauth_headers(req_headers['Authorization'], + oauth_client) + + +class OauthRequestIdTests(utils.TestRequestId, TokenTests): + + def setUp(self): + super(OauthRequestIdTests, self).setUp() + self.mgr = consumers.ConsumerManager(self.client) + + def _mock_request_method(self, method=None, body=None): + return self.useFixture(fixtures.MockPatchObject( + self.client, method, autospec=True, + return_value=(self.resp, body)) + ).mock + + def test_get_consumers(self): + body = {"consumer": {"name": "admin"}} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.get("admin") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with('/OS-OAUTH1/consumers/admin') + + def test_create_consumers(self): + body = {"consumer": {"name": "admin"}} + post_mock = self._mock_request_method(method='post', body=body) + + response = self.mgr.create(name="admin", description="fake") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + post_mock.assert_called_once_with('/OS-OAUTH1/consumers', body={ + 'consumer': {'name': 'admin', 'description': 'fake'}}) + + def test_update_consumers(self): + body = {"consumer": {"name": "admin"}} + patch_mock = self._mock_request_method(method='patch', body=body) + self._mock_request_method(method='post', body=body) + + response = self.mgr.update("admin", "demo") + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + patch_mock.assert_called_once_with('/OS-OAUTH1/consumers/admin', body={ + 'consumer': {'description': 'demo'}}) + + def test_delete_consumers(self): + get_mock = self._mock_request_method(method='delete') + + _, resp = self.mgr.delete("admin") + self.assertEqual(resp.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with('/OS-OAUTH1/consumers/admin') + + +class TestOAuthLibModule(utils.TestCase): + + def test_no_oauthlib_installed(self): + with mock.patch.object(auth, 'oauth1', None): + self.assertRaises(NotImplementedError, + auth.OAuth, + self.TEST_URL, + consumer_key=uuid.uuid4().hex, + consumer_secret=uuid.uuid4().hex, + access_key=uuid.uuid4().hex, + access_secret=uuid.uuid4().hex) diff --git a/keystoneclient/tests/unit/v3/test_policies.py b/keystoneclient/tests/unit/v3/test_policies.py new file mode 100644 index 000000000..47be4838d --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_policies.py @@ -0,0 +1,31 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import policies + + +class PolicyTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(PolicyTests, self).setUp() + self.key = 'policy' + self.collection_key = 'policies' + self.model = policies.Policy + self.manager = self.client.policies + + def new_ref(self, **kwargs): + kwargs = super(PolicyTests, self).new_ref(**kwargs) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('blob', uuid.uuid4().hex) + return kwargs diff --git a/keystoneclient/tests/unit/v3/test_projects.py b/keystoneclient/tests/unit/v3/test_projects.py new file mode 100644 index 000000000..99186f17f --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_projects.py @@ -0,0 +1,469 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import fixtures +import uuid + +from keystoneauth1 import exceptions as ksa_exceptions + +from keystoneclient import exceptions as ksc_exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import projects + + +class ProjectTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(ProjectTests, self).setUp() + self.key = 'project' + self.collection_key = 'projects' + self.model = projects.Project + self.manager = self.client.projects + + def new_ref(self, **kwargs): + kwargs = super(ProjectTests, self).new_ref(**kwargs) + return self._new_project_ref(ref=kwargs) + + def _new_project_ref(self, ref=None): + ref = ref or {} + ref.setdefault('domain_id', uuid.uuid4().hex) + ref.setdefault('enabled', True) + ref.setdefault('name', uuid.uuid4().hex) + return ref + + def test_list_projects_for_user(self): + ref_list = [self.new_ref(), self.new_ref()] + user_id = uuid.uuid4().hex + + self.stub_entity('GET', + ['users', user_id, self.collection_key], + entity=ref_list) + + returned_list = self.manager.list(user=user_id) + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_list_projects_for_domain(self): + ref_list = [self.new_ref(), self.new_ref()] + domain_id = uuid.uuid4().hex + + self.stub_entity('GET', [self.collection_key], entity=ref_list) + + returned_list = self.manager.list(domain=domain_id) + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + self.assertQueryStringIs('domain_id=%s' % domain_id) + + def test_list_projects_for_parent(self): + ref_list = [self.new_ref(), self.new_ref()] + parent_id = uuid.uuid4().hex + + self.stub_entity('GET', [self.collection_key], entity=ref_list) + + returned_list = self.manager.list(parent=parent_id) + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + self.assertQueryStringIs('parent_id=%s' % parent_id) + + def test_create_with_parent(self): + parent_ref = self.new_ref() + parent_ref['parent_id'] = uuid.uuid4().hex + parent = self.test_create(ref=parent_ref) + parent.id = parent_ref['id'] + + # Create another project under 'parent' in the hierarchy + ref = self.new_ref() + ref['parent_id'] = parent.id + + child_ref = ref.copy() + del child_ref['parent_id'] + child_ref['parent'] = parent + + # test_create() pops the 'id' of the mocked response + del ref['id'] + + # Resource objects may peform lazy-loading. The create() method of + # ProjectManager will try to access the 'uuid' attribute of the parent + # object, which will trigger a call to fetch the Resource attributes. + self.stub_entity('GET', id=parent_ref['id'], entity=parent_ref) + self.test_create(ref=child_ref, req_ref=ref) + + def test_create_with_parent_id(self): + ref = self._new_project_ref() + ref['parent_id'] = uuid.uuid4().hex + + self.stub_entity('POST', entity=ref, status_code=201) + + returned = self.manager.create(name=ref['name'], + domain=ref['domain_id'], + parent_id=ref['parent_id']) + + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(ref) + + def test_create_with_parent_and_parent_id(self): + ref = self._new_project_ref() + ref['parent_id'] = uuid.uuid4().hex + + self.stub_entity('POST', entity=ref, status_code=201) + + # Should ignore the 'parent_id' argument since we are also passing + # 'parent' + returned = self.manager.create(name=ref['name'], + domain=ref['domain_id'], + parent=ref['parent_id'], + parent_id=uuid.uuid4().hex) + + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(ref) + + def _create_projects_hierarchy(self, hierarchy_size=3): + """Create a project hierarchy with specified size. + + :param hierarchy_size: the desired hierarchy size, default is 3. + + :returns: a list of the projects in the created hierarchy. + + """ + ref = self.new_ref() + project_id = ref['id'] + projects = [ref] + + for i in range(1, hierarchy_size): + new_ref = self.new_ref() + new_ref['parent_id'] = project_id + projects.append(new_ref) + project_id = new_ref['id'] + + return projects + + def test_get_with_subtree_as_ids(self): + projects = self._create_projects_hierarchy() + ref = projects[0] + + # We will query for projects[0] subtree, it should include projects[1] + # and projects[2] structured like the following: + # { + # projects[1]: { + # projects[2]: None + # } + # } + ref['subtree'] = { + projects[1]['id']: { + projects[2]['id']: None + } + } + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id'], subtree_as_ids=True) + self.assertQueryStringIs('subtree_as_ids') + self.assertEqual(ref['subtree'], returned.subtree) + + def test_get_with_parents_as_ids(self): + projects = self._create_projects_hierarchy() + ref = projects[2] + + # We will query for projects[2] parents, it should include projects[1] + # and projects[0] structured like the following: + # { + # projects[1]: { + # projects[0]: None + # } + # } + ref['parents'] = { + projects[1]['id']: { + projects[0]['id']: None + } + } + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id'], parents_as_ids=True) + self.assertQueryStringIs('parents_as_ids') + self.assertEqual(ref['parents'], returned.parents) + + def test_get_with_parents_as_ids_and_subtree_as_ids(self): + ref = self.new_ref() + projects = self._create_projects_hierarchy() + ref = projects[1] + + # We will query for projects[1] subtree and parents. The subtree should + # include projects[2] and the parents should include projects[2]. + ref['parents'] = { + projects[0]['id']: None + } + ref['subtree'] = { + projects[2]['id']: None + } + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id'], + parents_as_ids=True, + subtree_as_ids=True) + self.assertQueryStringIs('subtree_as_ids&parents_as_ids') + self.assertEqual(ref['parents'], returned.parents) + self.assertEqual(ref['subtree'], returned.subtree) + + def test_get_with_subtree_as_list(self): + projects = self._create_projects_hierarchy() + ref = projects[0] + + ref['subtree_as_list'] = [] + for i in range(1, len(projects)): + ref['subtree_as_list'].append(projects[i]) + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id'], subtree_as_list=True) + self.assertQueryStringIs('subtree_as_list') + for i in range(1, len(projects)): + for attr in projects[i]: + child = getattr(returned, 'subtree_as_list')[i - 1] + self.assertEqual( + child[attr], + projects[i][attr], + 'Expected different %s' % attr) + + def test_get_with_parents_as_list(self): + projects = self._create_projects_hierarchy() + ref = projects[2] + + ref['parents_as_list'] = [] + for i in range(0, len(projects) - 1): + ref['parents_as_list'].append(projects[i]) + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id'], parents_as_list=True) + self.assertQueryStringIs('parents_as_list') + for i in range(0, len(projects) - 1): + for attr in projects[i]: + parent = getattr(returned, 'parents_as_list')[i] + self.assertEqual( + parent[attr], + projects[i][attr], + 'Expected different %s' % attr) + + def test_get_with_parents_as_list_and_subtree_as_list(self): + ref = self.new_ref() + projects = self._create_projects_hierarchy() + ref = projects[1] + + ref['parents_as_list'] = [projects[0]] + ref['subtree_as_list'] = [projects[2]] + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id'], + parents_as_list=True, + subtree_as_list=True) + self.assertQueryStringIs('subtree_as_list&parents_as_list') + + for attr in projects[0]: + parent = getattr(returned, 'parents_as_list')[0] + self.assertEqual( + parent[attr], + projects[0][attr], + 'Expected different %s' % attr) + + for attr in projects[2]: + child = getattr(returned, 'subtree_as_list')[0] + self.assertEqual( + child[attr], + projects[2][attr], + 'Expected different %s' % attr) + + def test_get_with_invalid_parameters_combination(self): + # subtree_as_list and subtree_as_ids can not be included at the + # same time in the call. + self.assertRaises(ksc_exceptions.ValidationError, + self.manager.get, + project=uuid.uuid4().hex, + subtree_as_list=True, + subtree_as_ids=True) + + # parents_as_list and parents_as_ids can not be included at the + # same time in the call. + self.assertRaises(ksc_exceptions.ValidationError, + self.manager.get, + project=uuid.uuid4().hex, + parents_as_list=True, + parents_as_ids=True) + + def test_update_with_parent_project(self): + ref = self.new_ref() + ref['parent_id'] = uuid.uuid4().hex + + self.stub_entity('PATCH', id=ref['id'], entity=ref, status_code=403) + req_ref = ref.copy() + req_ref.pop('id') + + # NOTE(rodrigods): this is the expected behaviour of the Identity + # server, a different implementation might not fail this request. + self.assertRaises(ksa_exceptions.Forbidden, self.manager.update, + ref['id'], **utils.parameterize(req_ref)) + + def test_add_tag(self): + ref = self.new_ref() + tag_name = "blue" + + self.stub_url("PUT", + parts=[self.collection_key, ref['id'], "tags", tag_name], + status_code=201) + self.manager.add_tag(ref['id'], tag_name) + + def test_update_tags(self): + new_tags = ["blue", "orange"] + ref = self.new_ref() + + self.stub_url("PUT", + parts=[self.collection_key, ref['id'], "tags"], + json={"tags": new_tags}, + status_code=200) + + ret = self.manager.update_tags(ref['id'], new_tags) + self.assertEqual(ret, new_tags) + + def test_delete_tag(self): + ref = self.new_ref() + tag_name = "blue" + + self.stub_url("DELETE", + parts=[self.collection_key, ref['id'], "tags", tag_name], + status_code=204) + + self.manager.delete_tag(ref['id'], tag_name) + + def test_delete_all_tags(self): + ref = self.new_ref() + + self.stub_url("PUT", + parts=[self.collection_key, ref['id'], "tags"], + json={"tags": []}, + status_code=200) + + ret = self.manager.update_tags(ref['id'], []) + self.assertEqual([], ret) + + def test_list_tags(self): + ref = self.new_ref() + tags = ["blue", "orange", "green"] + + self.stub_url("GET", + parts=[self.collection_key, ref['id'], "tags"], + json={"tags": tags}, + status_code=200) + + ret_tags = self.manager.list_tags(ref['id']) + self.assertEqual(tags, ret_tags) + + def test_check_tag(self): + ref = self.new_ref() + + tag_name = "blue" + self.stub_url("HEAD", + parts=[self.collection_key, ref['id'], "tags", tag_name], + status_code=204) + self.assertTrue(self.manager.check_tag(ref['id'], tag_name)) + + no_tag = "orange" + self.stub_url("HEAD", + parts=[self.collection_key, ref['id'], "tags", no_tag], + status_code=404) + self.assertFalse(self.manager.check_tag(ref['id'], no_tag)) + + def _build_project_response(self, tags): + project_id = uuid.uuid4().hex + ret = {"projects": [ + {"is_domain": False, + "description": "", + "tags": tags, + "enabled": True, + "id": project_id, + "parent_id": "default", + "domain_id": "default", + "name": project_id} + ]} + return ret + + +class ProjectsRequestIdTests(utils.TestRequestId): + + url = "/projects" + + def setUp(self): + super(ProjectsRequestIdTests, self).setUp() + self.mgr = projects.ProjectManager(self.client) + self.mgr.resource_class = projects.Project + + def _mock_request_method(self, method=None, body=None): + return self.useFixture(fixtures.MockPatchObject( + self.client, method, autospec=True, + return_value=(self.resp, body)) + ).mock + + def test_get_project(self): + body = {"project": {"name": "admin"}} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.get(project='admin') + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with(self.url + '/admin') + + def test_create_project(self): + body = {"project": {"name": "admin", "domain": "admin"}} + post_mock = self._mock_request_method(method='post', body=body) + + response = self.mgr.create('admin', 'admin') + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + post_mock.assert_called_once_with(self.url, body={'project': { + 'name': 'admin', 'enabled': True, 'domain_id': 'admin'}}) + + def test_list_project(self): + body = {"projects": [{"name": "admin"}, {"name": "admin"}]} + get_mock = self._mock_request_method(method='get', body=body) + + returned_list = self.mgr.list() + self.assertEqual(returned_list.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with(self.url + '?') + + def test_update_project(self): + body = {"project": {"name": "admin"}} + patch_mock = self._mock_request_method(method='patch', body=body) + + put_mock = self._mock_request_method(method='put', body=body) + + response = self.mgr.update("admin", domain='demo') + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + patch_mock.assert_called_once_with(self.url + '/admin', body={ + 'project': {'domain_id': 'demo'}}) + self.assertFalse(put_mock.called) + + def test_delete_project(self): + get_mock = self._mock_request_method(method='delete') + + _, resp = self.mgr.delete("admin") + self.assertEqual(resp.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with(self.url + '/admin') diff --git a/keystoneclient/tests/unit/v3/test_regions.py b/keystoneclient/tests/unit/v3/test_regions.py new file mode 100644 index 000000000..90ddfdc64 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_regions.py @@ -0,0 +1,37 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import uuid + + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import regions + + +class RegionTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(RegionTests, self).setUp() + self.key = 'region' + self.collection_key = 'regions' + self.model = regions.Region + self.manager = self.client.regions + + def new_ref(self, **kwargs): + kwargs = super(RegionTests, self).new_ref(**kwargs) + kwargs.setdefault('enabled', True) + kwargs.setdefault('id', uuid.uuid4().hex) + return kwargs + + def test_update_enabled_defaults_to_none(self): + super(RegionTests, self).test_update( + req_ref={'description': uuid.uuid4().hex}) diff --git a/keystoneclient/tests/unit/v3/test_registered_limits.py b/keystoneclient/tests/unit/v3/test_registered_limits.py new file mode 100644 index 000000000..1f612f8bb --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_registered_limits.py @@ -0,0 +1,76 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import registered_limits + + +class RegisteredLimitTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(RegisteredLimitTests, self).setUp() + self.key = 'registered_limit' + self.collection_key = 'registered_limits' + self.model = registered_limits.RegisteredLimit + self.manager = self.client.registered_limits + + def new_ref(self, **kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'service_id': uuid.uuid4().hex, + 'resource_name': uuid.uuid4().hex, + 'default_limit': 10, + 'description': uuid.uuid4().hex + } + ref.update(kwargs) + return ref + + def test_create(self): + # This test overrides the generic test case provided by the CrudTests + # class because the registered limits API supports creating multiple + # limits in a single POST request. As a result, it returns the + # registered limits as a list of all the created limits from the + # request. This is different from what the base test_create() method + # assumes about keystone's API. The changes here override the base test + # to closely model how the actual registered limit API behaves. + ref = self.new_ref() + manager_ref = ref.copy() + manager_ref.pop('id') + req_ref = [manager_ref.copy()] + + self.stub_entity('POST', entity=req_ref, status_code=201) + + returned = self.manager.create(**utils.parameterize(manager_ref)) + self.assertIsInstance(returned, self.model) + + expected_limit = req_ref.pop() + for attr in expected_limit: + self.assertEqual( + getattr(returned, attr), + expected_limit[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs([expected_limit]) + + def test_list_filter_by_service(self): + service_id = uuid.uuid4().hex + expected_query = {'service_id': service_id} + self.test_list(expected_query=expected_query, service=service_id) + + def test_list_filter_resource_name(self): + resource_name = uuid.uuid4().hex + self.test_list(resource_name=resource_name) + + def test_list_filter_region(self): + region_id = uuid.uuid4().hex + expected_query = {'region_id': region_id} + self.test_list(expected_query=expected_query, region=region_id) diff --git a/keystoneclient/tests/unit/v3/test_role_assignments.py b/keystoneclient/tests/unit/v3/test_role_assignments.py new file mode 100644 index 000000000..39b4b2355 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_role_assignments.py @@ -0,0 +1,334 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import role_assignments + + +class RoleAssignmentsTests(utils.ClientTestCase, utils.CrudTests): + + def setUp(self): + super(RoleAssignmentsTests, self).setUp() + self.key = 'role_assignment' + self.collection_key = 'role_assignments' + self.model = role_assignments.RoleAssignment + self.manager = self.client.role_assignments + self.TEST_USER_SYSTEM_LIST = [{ + 'role': { + 'id': self.TEST_ROLE_ID + }, + 'scope': { + 'system': { + 'all': True + } + }, + 'user': { + 'id': self.TEST_USER_ID + } + }] + self.TEST_GROUP_SYSTEM_LIST = [{ + 'role': { + 'id': self.TEST_ROLE_ID + }, + 'scope': { + 'system': { + 'all': True + } + }, + 'group': { + 'id': self.TEST_GROUP_ID + } + }] + self.TEST_USER_DOMAIN_LIST = [{ + 'role': { + 'id': self.TEST_ROLE_ID + }, + 'scope': { + 'domain': { + 'id': self.TEST_DOMAIN_ID + } + }, + 'user': { + 'id': self.TEST_USER_ID + } + }] + self.TEST_GROUP_PROJECT_LIST = [{ + 'group': { + 'id': self.TEST_GROUP_ID + }, + 'role': { + 'id': self.TEST_ROLE_ID + }, + 'scope': { + 'project': { + 'id': self.TEST_TENANT_ID + } + } + }] + self.TEST_USER_PROJECT_LIST = [{ + 'user': { + 'id': self.TEST_USER_ID + }, + 'role': { + 'id': self.TEST_ROLE_ID + }, + 'scope': { + 'project': { + 'id': self.TEST_TENANT_ID + } + } + }] + + self.TEST_ALL_RESPONSE_LIST = (self.TEST_USER_PROJECT_LIST + + self.TEST_GROUP_PROJECT_LIST + + self.TEST_USER_DOMAIN_LIST + + self.TEST_USER_SYSTEM_LIST + + self.TEST_GROUP_SYSTEM_LIST) + + def _assert_returned_list(self, ref_list, returned_list): + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_list_by_id(self): + # It doesn't make sense to "list role assignments by ID" at all, given + # that they don't have globally unique IDs in the first place. But + # calling RoleAssignmentsManager.list(id=...) should still raise a + # TypeError when given an unexpected keyword argument 'id', so we don't + # actually have to modify the test in the superclass... I just wanted + # to make a note here in case the superclass changes. + super(RoleAssignmentsTests, self).test_list_by_id() + + def test_list_params(self): + ref_list = self.TEST_USER_PROJECT_LIST + self.stub_entity('GET', + [self.collection_key, + '?scope.project.id=%s&user.id=%s' % + (self.TEST_TENANT_ID, self.TEST_USER_ID)], + entity=ref_list) + + returned_list = self.manager.list(user=self.TEST_USER_ID, + project=self.TEST_TENANT_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'scope.project.id': self.TEST_TENANT_ID, + 'user.id': self.TEST_USER_ID} + self.assertQueryStringContains(**kwargs) + + def test_all_assignments_list(self): + ref_list = self.TEST_ALL_RESPONSE_LIST + self.stub_entity('GET', + [self.collection_key], + entity=ref_list) + + returned_list = self.manager.list() + self._assert_returned_list(ref_list, returned_list) + + kwargs = {} + self.assertQueryStringContains(**kwargs) + + def test_project_assignments_list(self): + ref_list = self.TEST_GROUP_PROJECT_LIST + self.TEST_USER_PROJECT_LIST + self.stub_entity('GET', + [self.collection_key, + '?scope.project.id=%s' % self.TEST_TENANT_ID], + entity=ref_list) + + returned_list = self.manager.list(project=self.TEST_TENANT_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'scope.project.id': self.TEST_TENANT_ID} + self.assertQueryStringContains(**kwargs) + + def test_project_assignments_list_include_subtree(self): + ref_list = self.TEST_GROUP_PROJECT_LIST + self.TEST_USER_PROJECT_LIST + self.stub_entity('GET', + [self.collection_key, + '?scope.project.id=%s&include_subtree=True' % + self.TEST_TENANT_ID], + entity=ref_list) + + returned_list = self.manager.list(project=self.TEST_TENANT_ID, + include_subtree=True) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'scope.project.id': self.TEST_TENANT_ID, + 'include_subtree': 'True'} + self.assertQueryStringContains(**kwargs) + + def test_domain_assignments_list(self): + ref_list = self.TEST_USER_DOMAIN_LIST + self.stub_entity('GET', + [self.collection_key, + '?scope.domain.id=%s' % self.TEST_DOMAIN_ID], + entity=ref_list) + + returned_list = self.manager.list(domain=self.TEST_DOMAIN_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'scope.domain.id': self.TEST_DOMAIN_ID} + self.assertQueryStringContains(**kwargs) + + def test_system_assignment_list(self): + ref_list = self.TEST_USER_SYSTEM_LIST + self.TEST_GROUP_SYSTEM_LIST + + self.stub_entity('GET', + [self.collection_key, '?scope.system=all'], + entity=ref_list) + + returned_list = self.manager.list(system='all') + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'scope.system': 'all'} + self.assertQueryStringContains(**kwargs) + + def test_system_assignment_list_for_user(self): + ref_list = self.TEST_USER_SYSTEM_LIST + + self.stub_entity('GET', + [self.collection_key, + '?user.id=%s&scope.system=all' % self.TEST_USER_ID], + entity=ref_list) + + returned_list = self.manager.list(system='all', user=self.TEST_USER_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'scope.system': 'all', 'user.id': self.TEST_USER_ID} + self.assertQueryStringContains(**kwargs) + + def test_system_assignment_list_for_group(self): + ref_list = self.TEST_GROUP_SYSTEM_LIST + + self.stub_entity( + 'GET', + [self.collection_key, + '?group.id=%s&scope.system=all' % self.TEST_GROUP_ID], + entity=ref_list) + + returned_list = self.manager.list( + system='all', group=self.TEST_GROUP_ID + ) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'scope.system': 'all', 'group.id': self.TEST_GROUP_ID} + self.assertQueryStringContains(**kwargs) + + def test_group_assignments_list(self): + ref_list = self.TEST_GROUP_PROJECT_LIST + self.stub_entity('GET', + [self.collection_key, + '?group.id=%s' % self.TEST_GROUP_ID], + entity=ref_list) + + returned_list = self.manager.list(group=self.TEST_GROUP_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'group.id': self.TEST_GROUP_ID} + self.assertQueryStringContains(**kwargs) + + def test_user_assignments_list(self): + ref_list = self.TEST_USER_DOMAIN_LIST + self.TEST_USER_PROJECT_LIST + self.stub_entity('GET', + [self.collection_key, + '?user.id=%s' % self.TEST_USER_ID], + entity=ref_list) + + returned_list = self.manager.list(user=self.TEST_USER_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'user.id': self.TEST_USER_ID} + self.assertQueryStringContains(**kwargs) + + def test_effective_assignments_list(self): + ref_list = self.TEST_USER_PROJECT_LIST + self.TEST_USER_DOMAIN_LIST + self.stub_entity('GET', + [self.collection_key, + '?effective=True'], + entity=ref_list) + + returned_list = self.manager.list(effective=True) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'effective': 'True'} + self.assertQueryStringContains(**kwargs) + + def test_include_names_assignments_list(self): + ref_list = self.TEST_ALL_RESPONSE_LIST + self.stub_entity('GET', + [self.collection_key, + '?include_names=True'], + entity=ref_list) + + returned_list = self.manager.list(include_names=True) + self._assert_returned_list(ref_list, returned_list) + kwargs = {'include_names': 'True'} + self.assertQueryStringContains(**kwargs) + + def test_role_assignments_list(self): + ref_list = self.TEST_ALL_RESPONSE_LIST + self.stub_entity('GET', + [self.collection_key, + '?role.id=' + self.TEST_ROLE_ID], + entity=ref_list) + + returned_list = self.manager.list(role=self.TEST_ROLE_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'role.id': self.TEST_ROLE_ID} + self.assertQueryStringContains(**kwargs) + + def test_role_assignments_inherited_list(self): + ref_list = self.TEST_ALL_RESPONSE_LIST + self.stub_entity('GET', + [self.collection_key, + '?scope.OS-INHERIT:inherited_to=projects'], + entity=ref_list + ) + + returned_list = self.manager.list( + os_inherit_extension_inherited_to='projects') + self._assert_returned_list(ref_list, returned_list) + + query_string = 'scope.OS-INHERIT:inherited_to=projects' + self.assertQueryStringIs(query_string) + + def test_domain_and_project_list(self): + # Should only accept either domain or project, never both + self.assertRaises(exceptions.ValidationError, + self.manager.list, + domain=self.TEST_DOMAIN_ID, + project=self.TEST_TENANT_ID) + + def test_user_and_group_list(self): + # Should only accept either user or group, never both + self.assertRaises(exceptions.ValidationError, self.manager.list, + user=self.TEST_USER_ID, group=self.TEST_GROUP_ID) + + def test_create(self): + # Create not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.create) + + def test_update(self): + # Update not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.update) + + def test_delete(self): + # Delete not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.delete) + + def test_get(self): + # Get not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.get) + + def test_find(self): + # Find not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.find) diff --git a/keystoneclient/tests/unit/v3/test_roles.py b/keystoneclient/tests/unit/v3/test_roles.py new file mode 100644 index 000000000..0e531e736 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_roles.py @@ -0,0 +1,824 @@ +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import roles +from testtools import matchers + + +class RoleTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(RoleTests, self).setUp() + self.key = 'role' + self.collection_key = 'roles' + self.model = roles.Role + self.manager = self.client.roles + + def new_ref(self, **kwargs): + kwargs = super(RoleTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def _new_domain_ref(self, **kwargs): + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_create_with_domain_id(self): + ref = self.new_ref() + ref['domain_id'] = uuid.uuid4().hex + self.test_create(ref=ref) + + def test_create_with_domain(self): + ref = self.new_ref() + domain_ref = self._new_domain_ref() + domain_ref['id'] = uuid.uuid4().hex + ref['domain_id'] = domain_ref['id'] + + self.stub_entity('POST', entity=ref, status_code=201) + returned = self.manager.create(name=ref['name'], + domain=domain_ref) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + def test_domain_role_grant(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=201) + + self.manager.grant(role=ref['id'], domain=domain_id, user=user_id) + + def test_domain_role_grant_inherited(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['OS-INHERIT', 'domains', domain_id, 'users', user_id, + self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=201) + + self.manager.grant(role=ref['id'], domain=domain_id, user=user_id, + os_inherit_extension_inherited=True) + + def test_project_role_grant_inherited(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['OS-INHERIT', 'projects', project_id, 'users', user_id, + self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=204) + + self.manager.grant(role=ref['id'], project=project_id, user=user_id, + os_inherit_extension_inherited=True) + + def test_domain_group_role_grant(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=201) + + self.manager.grant(role=ref['id'], domain=domain_id, group=group_id) + + def test_domain_group_role_grant_inherited(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['OS-INHERIT', 'domains', domain_id, 'groups', group_id, + self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=201) + + self.manager.grant(role=ref['id'], domain=domain_id, group=group_id, + os_inherit_extension_inherited=True) + + def test_project_group_role_grant_inherited(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['OS-INHERIT', 'projects', project_id, 'groups', + group_id, self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=204) + + self.manager.grant(role=ref['id'], project=project_id, group=group_id, + os_inherit_extension_inherited=True) + + def test_domain_role_list(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['domains', domain_id, 'users', user_id, + self.collection_key], entity=ref_list) + + self.manager.list(domain=domain_id, user=user_id) + + def test_domain_role_list_inherited(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['OS-INHERIT', + 'domains', domain_id, 'users', user_id, + self.collection_key, 'inherited_to_projects'], + entity=ref_list) + + returned_list = self.manager.list(domain=domain_id, user=user_id, + os_inherit_extension_inherited=True) + + self.assertThat(ref_list, matchers.HasLength(len(returned_list))) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_project_user_role_list_inherited(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['OS-INHERIT', + 'projects', project_id, 'users', user_id, + self.collection_key, 'inherited_to_projects'], + entity=ref_list) + + returned_list = self.manager.list(project=project_id, user=user_id, + os_inherit_extension_inherited=True) + + self.assertThat(ref_list, matchers.HasLength(len(returned_list))) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_domain_group_role_list(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['domains', domain_id, 'groups', group_id, + self.collection_key], entity=ref_list) + + self.manager.list(domain=domain_id, group=group_id) + + def test_domain_group_role_list_inherited(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['OS-INHERIT', + 'domains', domain_id, 'groups', group_id, + self.collection_key, 'inherited_to_projects'], + entity=ref_list) + + returned_list = self.manager.list(domain=domain_id, group=group_id, + os_inherit_extension_inherited=True) + + self.assertThat(ref_list, matchers.HasLength(len(returned_list))) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_project_group_role_list_inherited(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['OS-INHERIT', + 'projects', project_id, 'groups', group_id, + self.collection_key, 'inherited_to_projects'], + entity=ref_list) + + returned_list = self.manager.list(project=project_id, group=group_id, + os_inherit_extension_inherited=True) + + self.assertThat(ref_list, matchers.HasLength(len(returned_list))) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_domain_role_check(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.check(role=ref['id'], domain=domain_id, + user=user_id) + + def test_domain_role_check_inherited(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['OS-INHERIT', + 'domains', domain_id, 'users', user_id, + self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=204) + + self.manager.check(role=ref['id'], domain=domain_id, + user=user_id, os_inherit_extension_inherited=True) + + def test_project_role_check_inherited(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['OS-INHERIT', + 'projects', project_id, 'users', user_id, + self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=204) + + self.manager.check(role=ref['id'], project=project_id, + user=user_id, os_inherit_extension_inherited=True) + + def test_domain_group_role_check(self): + return + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.check(role=ref['id'], domain=domain_id, group=group_id) + + def test_domain_group_role_check_inherited(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['OS-INHERIT', + 'domains', domain_id, 'groups', group_id, + self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=204) + + self.manager.check(role=ref['id'], domain=domain_id, + group=group_id, os_inherit_extension_inherited=True) + + def test_project_group_role_check_inherited(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['OS-INHERIT', + 'projects', project_id, 'groups', group_id, + self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=204) + + self.manager.check(role=ref['id'], project=project_id, + group=group_id, os_inherit_extension_inherited=True) + + def test_domain_role_revoke(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.revoke(role=ref['id'], domain=domain_id, user=user_id) + + def test_domain_group_role_revoke(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.revoke(role=ref['id'], domain=domain_id, group=group_id) + + def test_domain_role_revoke_inherited(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['OS-INHERIT', 'domains', domain_id, 'users', user_id, + self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=204) + + self.manager.revoke(role=ref['id'], domain=domain_id, + user=user_id, os_inherit_extension_inherited=True) + + def test_project_role_revoke_inherited(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['OS-INHERIT', 'projects', project_id, 'users', user_id, + self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=204) + + self.manager.revoke(role=ref['id'], project=project_id, + user=user_id, os_inherit_extension_inherited=True) + + def test_domain_group_role_revoke_inherited(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['OS-INHERIT', 'domains', domain_id, 'groups', group_id, + self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=200) + + self.manager.revoke(role=ref['id'], domain=domain_id, + group=group_id, + os_inherit_extension_inherited=True) + + def test_project_group_role_revoke_inherited(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['OS-INHERIT', 'projects', project_id, 'groups', + group_id, self.collection_key, ref['id'], + 'inherited_to_projects'], + status_code=204) + + self.manager.revoke(role=ref['id'], project=project_id, + group=group_id, + os_inherit_extension_inherited=True) + + def test_project_role_grant(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=201) + + self.manager.grant(role=ref['id'], project=project_id, user=user_id) + + def test_project_group_role_grant(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('PUT', + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=201) + + self.manager.grant(role=ref['id'], project=project_id, group=group_id) + + def test_project_role_list(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['projects', project_id, 'users', user_id, + self.collection_key], entity=ref_list) + + self.manager.list(project=project_id, user=user_id) + + def test_project_group_role_list(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['projects', project_id, 'groups', group_id, + self.collection_key], entity=ref_list) + + self.manager.list(project=project_id, group=group_id) + + def test_project_role_check(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=200) + + self.manager.check(role=ref['id'], project=project_id, user=user_id) + + def test_project_group_role_check(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=200) + + self.manager.check(role=ref['id'], project=project_id, group=group_id) + + def test_project_role_revoke(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.revoke(role=ref['id'], project=project_id, user=user_id) + + def test_project_group_role_revoke(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status_code=204) + + self.manager.revoke(role=ref['id'], project=project_id, group=group_id) + + def test_domain_project_role_grant_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.grant, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_list_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + + self.assertRaises( + exceptions.ValidationError, + self.manager.list, + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_check_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.check, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_revoke_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.revoke, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_user_group_role_grant_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.grant, + role=ref['id'], + project=project_id, + group=group_id, + user=user_id) + + def test_user_group_role_list_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.assertRaises( + exceptions.ValidationError, + self.manager.list, + project=project_id, + group=group_id, + user=user_id) + + def test_user_group_role_check_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.check, + role=ref['id'], + project=project_id, + group=group_id, + user=user_id) + + def test_user_group_role_revoke_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.revoke, + role=ref['id'], + project=project_id, + group=group_id, + user=user_id) + + +class DeprecatedImpliedRoleTests(utils.ClientTestCase): + def setUp(self): + super(DeprecatedImpliedRoleTests, self).setUp() + self.key = 'role' + self.collection_key = 'roles' + self.model = roles.Role + self.manager = self.client.roles + + def test_implied_create(self): + prior_id = uuid.uuid4().hex + prior_name = uuid.uuid4().hex + implied_id = uuid.uuid4().hex + implied_name = uuid.uuid4().hex + + mock_response = { + "role_inference": { + "implies": { + "id": implied_id, + "links": {"self": "http://host/v3/roles/%s" % implied_id}, + "name": implied_name + }, + "prior_role": { + "id": prior_id, + "links": {"self": "http://host/v3/roles/%s" % prior_id}, + "name": prior_name + } + } + } + + self.stub_url('PUT', + ['roles', prior_id, 'implies', implied_id], + json=mock_response, + status_code=201) + + with self.deprecations.expect_deprecations_here(): + manager_result = self.manager.create_implied(prior_id, implied_id) + self.assertIsInstance(manager_result, roles.InferenceRule) + self.assertEqual(mock_response['role_inference']['implies'], + manager_result.implies) + self.assertEqual(mock_response['role_inference']['prior_role'], + manager_result.prior_role) + + +class ImpliedRoleTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(ImpliedRoleTests, self).setUp() + self.key = 'role_inference' + self.collection_key = 'role_inferences' + self.model = roles.InferenceRule + self.manager = self.client.inference_rules + + def test_check(self): + prior_role_id = uuid.uuid4().hex + implied_role_id = uuid.uuid4().hex + self.stub_url('HEAD', + ['roles', prior_role_id, 'implies', implied_role_id], + status_code=204) + + result = self.manager.check(prior_role_id, implied_role_id) + self.assertTrue(result) + + def test_get(self): + prior_id = uuid.uuid4().hex + prior_name = uuid.uuid4().hex + implied_id = uuid.uuid4().hex + implied_name = uuid.uuid4().hex + + mock_response = { + "role_inference": { + "implies": { + "id": implied_id, + "links": {"self": "http://host/v3/roles/%s" % implied_id}, + "name": implied_name + }, + "prior_role": { + "id": prior_id, + "links": {"self": "http://host/v3/roles/%s" % prior_id}, + "name": prior_name + } + } + } + + self.stub_url('GET', + ['roles', prior_id, 'implies', implied_id], + json=mock_response, + status_code=200) + + manager_result = self.manager.get(prior_id, implied_id) + self.assertIsInstance(manager_result, roles.InferenceRule) + self.assertEqual(mock_response['role_inference']['implies'], + manager_result.implies) + self.assertEqual(mock_response['role_inference']['prior_role'], + manager_result.prior_role) + + def test_create(self): + prior_id = uuid.uuid4().hex + prior_name = uuid.uuid4().hex + implied_id = uuid.uuid4().hex + implied_name = uuid.uuid4().hex + + mock_response = { + "role_inference": { + "implies": { + "id": implied_id, + "links": {"self": "http://host/v3/roles/%s" % implied_id}, + "name": implied_name + }, + "prior_role": { + "id": prior_id, + "links": {"self": "http://host/v3/roles/%s" % prior_id}, + "name": prior_name + } + } + } + + self.stub_url('PUT', + ['roles', prior_id, 'implies', implied_id], + json=mock_response, + status_code=201) + + manager_result = self.manager.create(prior_id, implied_id) + + self.assertIsInstance(manager_result, roles.InferenceRule) + self.assertEqual(mock_response['role_inference']['implies'], + manager_result.implies) + self.assertEqual(mock_response['role_inference']['prior_role'], + manager_result.prior_role) + + def test_delete(self): + prior_role_id = uuid.uuid4().hex + implied_role_id = uuid.uuid4().hex + self.stub_url('DELETE', + ['roles', prior_role_id, 'implies', implied_role_id], + status_code=204) + + status, body = self.manager.delete(prior_role_id, implied_role_id) + self.assertEqual(204, status.status_code) + self.assertIsNone(body) + + def test_list_role_inferences(self): + prior_id = uuid.uuid4().hex + prior_name = uuid.uuid4().hex + implied_id = uuid.uuid4().hex + implied_name = uuid.uuid4().hex + + mock_response = { + "role_inferences": [{ + "implies": [{ + "id": implied_id, + "links": {"self": "http://host/v3/roles/%s" % implied_id}, + "name": implied_name + }], + "prior_role": { + "id": prior_id, + "links": {"self": "http://host/v3/roles/%s" % prior_id}, + "name": prior_name + } + }] + } + + self.stub_url('GET', + ['role_inferences'], + json=mock_response, + status_code=200) + manager_result = self.manager.list_inference_roles() + self.assertEqual(1, len(manager_result)) + self.assertIsInstance(manager_result[0], roles.InferenceRule) + self.assertEqual(mock_response['role_inferences'][0]['implies'], + manager_result[0].implies) + self.assertEqual(mock_response['role_inferences'][0]['prior_role'], + manager_result[0].prior_role) + + def test_list(self): + prior_id = uuid.uuid4().hex + prior_name = uuid.uuid4().hex + implied_id = uuid.uuid4().hex + implied_name = uuid.uuid4().hex + + mock_response = { + "role_inference": { + "implies": [{ + "id": implied_id, + "links": {"self": "http://host/v3/roles/%s" % implied_id}, + "name": implied_name + }], + "prior_role": { + "id": prior_id, + "links": {"self": "http://host/v3/roles/%s" % prior_id}, + "name": prior_name + } + }, + "links": {"self": "http://host/v3/roles/%s/implies" % prior_id} + } + + self.stub_url('GET', + ['roles', prior_id, 'implies'], + json=mock_response, + status_code=200) + + manager_result = self.manager.list(prior_id) + self.assertIsInstance(manager_result, roles.InferenceRule) + self.assertEqual(1, len(manager_result.implies)) + self.assertEqual(mock_response['role_inference']['implies'], + manager_result.implies) + self.assertEqual(mock_response['role_inference']['prior_role'], + manager_result.prior_role) + + def test_update(self): + # Update not supported for rule inferences + self.assertRaises(exceptions.MethodNotImplemented, self.manager.update) + + def test_find(self): + # Find not supported for rule inferences + self.assertRaises(exceptions.MethodNotImplemented, self.manager.find) + + def test_put(self): + # Put not supported for rule inferences + self.assertRaises(exceptions.MethodNotImplemented, self.manager.put) + + def test_list_params(self): + # Put not supported for rule inferences + self.skipTest("list params not supported by rule inferences") diff --git a/keystoneclient/tests/unit/v3/test_service_catalog.py b/keystoneclient/tests/unit/v3/test_service_catalog.py new file mode 100644 index 000000000..bdd92cebd --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_service_catalog.py @@ -0,0 +1,279 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneauth1 import fixture + +from keystoneclient import access +from keystoneclient import exceptions +from keystoneclient.tests.unit import utils as test_utils +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import utils + + +class ServiceCatalogTest(utils.TestCase): + def setUp(self): + super(ServiceCatalogTest, self).setUp() + self.AUTH_RESPONSE_BODY = client_fixtures.auth_response_body() + self.RESPONSE = test_utils.test_response( + headers=client_fixtures.AUTH_RESPONSE_HEADERS + ) + + self.north_endpoints = {'public': + 'http://glance.north.host/glanceapi/public', + 'internal': + 'http://glance.north.host/glanceapi/internal', + 'admin': + 'http://glance.north.host/glanceapi/admin'} + + self.south_endpoints = {'public': + 'http://glance.south.host/glanceapi/public', + 'internal': + 'http://glance.south.host/glanceapi/internal', + 'admin': + 'http://glance.south.host/glanceapi/admin'} + + def test_building_a_service_catalog(self): + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + self.assertEqual(sc.url_for(service_type='compute'), + "https://compute.north.host/novapi/public") + self.assertEqual(sc.url_for(service_type='compute', + endpoint_type='internal'), + "https://compute.north.host/novapi/internal") + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", + "South", service_type='compute') + + def test_service_catalog_endpoints(self): + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + public_ep = sc.get_endpoints(service_type='compute', + endpoint_type='public') + self.assertEqual(public_ep['compute'][0]['region'], 'North') + self.assertEqual(public_ep['compute'][0]['url'], + "https://compute.north.host/novapi/public") + + def test_service_catalog_regions(self): + self.AUTH_RESPONSE_BODY['token']['region_name'] = "North" + # Setting region_name on the catalog is deprecated. + with self.deprecations.expect_deprecations_here(): + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', endpoint_type='public') + self.assertEqual(url, "http://glance.north.host/glanceapi/public") + + self.AUTH_RESPONSE_BODY['token']['region_name'] = "South" + # Setting region_name on the catalog is deprecated. + with self.deprecations.expect_deprecations_here(): + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + url = sc.url_for(service_type='image', endpoint_type='internal') + self.assertEqual(url, "http://glance.south.host/glanceapi/internal") + + def test_service_catalog_empty(self): + self.AUTH_RESPONSE_BODY['token']['catalog'] = [] + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + self.assertRaises(exceptions.EmptyCatalog, + auth_ref.service_catalog.url_for, + service_type='image', + endpoint_type='internalURL') + + def test_service_catalog_get_endpoints_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + endpoints = sc.get_endpoints(service_type='image', region_name='North') + self.assertEqual(len(endpoints), 1) + for endpoint in endpoints['image']: + self.assertEqual(endpoint['url'], + self.north_endpoints[endpoint['interface']]) + + endpoints = sc.get_endpoints(service_type='image', region_name='South') + self.assertEqual(len(endpoints), 1) + for endpoint in endpoints['image']: + self.assertEqual(endpoint['url'], + self.south_endpoints[endpoint['interface']]) + + endpoints = sc.get_endpoints(service_type='compute') + self.assertEqual(len(endpoints['compute']), 3) + + endpoints = sc.get_endpoints(service_type='compute', + region_name='North') + self.assertEqual(len(endpoints['compute']), 3) + + endpoints = sc.get_endpoints(service_type='compute', + region_name='West') + self.assertEqual(len(endpoints['compute']), 0) + + def test_service_catalog_url_for_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', region_name='North') + self.assertEqual(url, self.north_endpoints['public']) + + url = sc.url_for(service_type='image', region_name='South') + self.assertEqual(url, self.south_endpoints['public']) + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, + service_type='image', region_name='West') + + def test_servcie_catalog_get_url_region_names(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + urls = sc.get_urls(service_type='image') + self.assertEqual(len(urls), 2) + + urls = sc.get_urls(service_type='image', region_name='North') + self.assertEqual(len(urls), 1) + self.assertEqual(urls[0], self.north_endpoints['public']) + + urls = sc.get_urls(service_type='image', region_name='South') + self.assertEqual(len(urls), 1) + self.assertEqual(urls[0], self.south_endpoints['public']) + + urls = sc.get_urls(service_type='image', region_name='West') + self.assertIsNone(urls) + + def test_service_catalog_param_overrides_body_region(self): + self.AUTH_RESPONSE_BODY['token']['region_name'] = "North" + # Passing region_name to service catalog is deprecated. + with self.deprecations.expect_deprecations_here(): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image') + self.assertEqual(url, self.north_endpoints['public']) + + url = sc.url_for(service_type='image', region_name='South') + self.assertEqual(url, self.south_endpoints['public']) + + endpoints = sc.get_endpoints(service_type='image') + self.assertEqual(len(endpoints['image']), 3) + for endpoint in endpoints['image']: + self.assertEqual(endpoint['url'], + self.north_endpoints[endpoint['interface']]) + + endpoints = sc.get_endpoints(service_type='image', region_name='South') + self.assertEqual(len(endpoints['image']), 3) + for endpoint in endpoints['image']: + self.assertEqual(endpoint['url'], + self.south_endpoints[endpoint['interface']]) + + def test_service_catalog_service_name(self): + auth_ref = access.AccessInfo.factory(resp=None, + body=self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_name='glance', endpoint_type='public', + service_type='image', region_name='North') + self.assertEqual('http://glance.north.host/glanceapi/public', url) + + url = sc.url_for(service_name='glance', endpoint_type='public', + service_type='image', region_name='South') + self.assertEqual('http://glance.south.host/glanceapi/public', url) + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, + service_name='glance', service_type='compute') + + urls = sc.get_urls(service_type='image', service_name='glance', + endpoint_type='public') + + self.assertIn('http://glance.north.host/glanceapi/public', urls) + self.assertIn('http://glance.south.host/glanceapi/public', urls) + + urls = sc.get_urls(service_type='image', service_name='Servers', + endpoint_type='public') + + self.assertIsNone(urls) + + def test_service_catalog_without_name(self): + pr_auth_ref = access.AccessInfo.factory( + resp=None, + body=client_fixtures.project_scoped_token()) + pr_sc = pr_auth_ref.service_catalog + + # this will work because there are no service names on that token + url_ref = 'http://public.com:8774/v2/225da22d3ce34b15877ea70b2a575f58' + url = pr_sc.url_for(service_type='compute', service_name='NotExist', + endpoint_type='public') + self.assertEqual(url_ref, url) + + ab_auth_ref = access.AccessInfo.factory(resp=None, + body=self.AUTH_RESPONSE_BODY) + ab_sc = ab_auth_ref.service_catalog + + # this won't work because there is a name and it's not this one + self.assertRaises(exceptions.EndpointNotFound, ab_sc.url_for, + service_type='compute', service_name='NotExist', + endpoint_type='public') + + +class ServiceCatalogV3Test(ServiceCatalogTest): + + def test_building_a_service_catalog(self): + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + self.assertEqual(sc.url_for(service_type='compute'), + 'https://compute.north.host/novapi/public') + self.assertEqual(sc.url_for(service_type='compute', + endpoint_type='internal'), + 'https://compute.north.host/novapi/internal') + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, 'region_id', + 'South', service_type='compute') + + def test_service_catalog_endpoints(self): + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + public_ep = sc.get_endpoints(service_type='compute', + endpoint_type='public') + self.assertEqual(public_ep['compute'][0]['region_id'], 'North') + self.assertEqual(public_ep['compute'][0]['url'], + 'https://compute.north.host/novapi/public') + + def test_service_catalog_multiple_service_types(self): + token = fixture.V3Token() + token.set_project_scope() + + for i in range(3): + s = token.add_service('compute') + s.add_standard_endpoints(public='public-%d' % i, + admin='admin-%d' % i, + internal='internal-%d' % i, + region='region-%d' % i) + + auth_ref = access.AccessInfo.factory(resp=None, body=token) + + urls = auth_ref.service_catalog.get_urls(service_type='compute', + endpoint_type='public') + + self.assertEqual(set(['public-0', 'public-1', 'public-2']), set(urls)) + + urls = auth_ref.service_catalog.get_urls(service_type='compute', + endpoint_type='public', + region_name='region-1') + + self.assertEqual(('public-1', ), urls) diff --git a/keystoneclient/tests/unit/v3/test_services.py b/keystoneclient/tests/unit/v3/test_services.py new file mode 100644 index 000000000..9bda5dbb5 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_services.py @@ -0,0 +1,44 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import services + + +class ServiceTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(ServiceTests, self).setUp() + self.key = 'service' + self.collection_key = 'services' + self.model = services.Service + self.manager = self.client.services + + def new_ref(self, **kwargs): + kwargs = super(ServiceTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs + + def test_list_filter_name(self): + filter_name = uuid.uuid4().hex + expected_query = {'name': filter_name} + super(ServiceTests, self).test_list(expected_query=expected_query, + name=filter_name) + + def test_list_filter_type(self): + filter_type = uuid.uuid4().hex + expected_query = {'type': filter_type} + super(ServiceTests, self).test_list(expected_query=expected_query, + type=filter_type) diff --git a/keystoneclient/tests/unit/v3/test_simple_cert.py b/keystoneclient/tests/unit/v3/test_simple_cert.py new file mode 100644 index 000000000..a059f08d7 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_simple_cert.py @@ -0,0 +1,73 @@ +# Copyright 2014 IBM Corp. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import fixtures +import testresources + +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib import simple_cert + + +class SimpleCertTests(utils.ClientTestCase, testresources.ResourcedTestCase): + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def test_get_ca_certificate(self): + self.stub_url('GET', ['OS-SIMPLE-CERT', 'ca'], + headers={'Content-Type': 'application/x-pem-file'}, + text=self.examples.SIGNING_CA) + res = self.client.simple_cert.get_ca_certificates() + self.assertEqual(self.examples.SIGNING_CA, res) + + def test_get_certificates(self): + self.stub_url('GET', ['OS-SIMPLE-CERT', 'certificates'], + headers={'Content-Type': 'application/x-pem-file'}, + text=self.examples.SIGNING_CERT) + res = self.client.simple_cert.get_certificates() + self.assertEqual(self.examples.SIGNING_CERT, res) + + +class SimpleCertRequestIdTests(utils.TestRequestId): + + def setUp(self): + super(SimpleCertRequestIdTests, self).setUp() + self.mgr = simple_cert.SimpleCertManager(self.client) + + def _mock_request_method(self, method=None, body=None): + return self.useFixture(fixtures.MockPatchObject( + self.client, method, autospec=True, + return_value=(self.resp, body)) + ).mock + + def test_list_ca_certificates(self): + body = {"certificates": [{"name": "admin"}, {"name": "admin2"}]} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.get_ca_certificates() + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with( + '/OS-SIMPLE-CERT/ca', authenticated=False) + + def test_list_certificates(self): + body = {"certificates": [{"name": "admin"}, {"name": "admin2"}]} + get_mock = self._mock_request_method(method='get', body=body) + + response = self.mgr.get_certificates() + self.assertEqual(response.request_ids[0], self.TEST_REQUEST_ID) + get_mock.assert_called_once_with( + '/OS-SIMPLE-CERT/certificates', authenticated=False) + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/v3/test_tokens.py b/keystoneclient/tests/unit/v3/test_tokens.py new file mode 100644 index 000000000..1a8fd6831 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_tokens.py @@ -0,0 +1,158 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneauth1 import exceptions +import testresources + +from keystoneclient import access +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit.v3 import utils + + +class TokenTests(utils.ClientTestCase, testresources.ResourcedTestCase): + + resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] + + def test_revoke_token_with_token_id(self): + token_id = uuid.uuid4().hex + self.stub_url('DELETE', ['/auth/tokens'], status_code=204) + self.client.tokens.revoke_token(token_id) + self.assertRequestHeaderEqual('X-Subject-Token', token_id) + + def test_revoke_token_with_access_info_instance(self): + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_DEFAULT] + token = access.AccessInfoV3(token_id, token_ref['token']) + self.stub_url('DELETE', ['/auth/tokens'], status_code=204) + self.client.tokens.revoke_token(token) + self.assertRequestHeaderEqual('X-Subject-Token', token_id) + + def test_get_revoked(self): + sample_revoked_response = {'signed': '-----BEGIN CMS-----\nMIIB...'} + self.stub_url('GET', ['auth', 'tokens', 'OS-PKI', 'revoked'], + json=sample_revoked_response) + resp = self.client.tokens.get_revoked() + self.assertQueryStringIs() + self.assertEqual(sample_revoked_response, resp) + + def test_get_revoked_audit_id_only(self): + # When get_revoked(audit_id_only=True) then ?audit_id_only is set on + # the request. + sample_revoked_response = { + 'revoked': [ + { + 'audit_id': uuid.uuid4().hex, + 'expires': '2016-01-21T15:53:52Z', + }, + ], + } + self.stub_url('GET', ['auth', 'tokens', 'OS-PKI', 'revoked'], + json=sample_revoked_response) + resp = self.client.tokens.get_revoked(audit_id_only=True) + self.assertQueryStringIs('audit_id_only') + self.assertEqual(sample_revoked_response, resp) + + def test_validate_token_with_token_id(self): + # Can validate a token passing a string token ID. + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_DEFAULT] + self.stub_url('GET', ['auth', 'tokens'], + headers={'X-Subject-Token': token_id, }, json=token_ref) + + token_data = self.client.tokens.get_token_data(token_id) + self.assertEqual(token_data, token_ref) + + access_info = self.client.tokens.validate(token_id) + + self.assertRequestHeaderEqual('X-Subject-Token', token_id) + self.assertIsInstance(access_info, access.AccessInfoV3) + self.assertEqual(token_id, access_info.auth_token) + + def test_validate_token_with_access_info(self): + # Can validate a token passing an access info. + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_DEFAULT] + token = access.AccessInfoV3(token_id, token_ref['token']) + self.stub_url('GET', ['auth', 'tokens'], + headers={'X-Subject-Token': token_id, }, json=token_ref) + access_info = self.client.tokens.validate(token) + + self.assertRequestHeaderEqual('X-Subject-Token', token_id) + self.assertIsInstance(access_info, access.AccessInfoV3) + self.assertEqual(token_id, access_info.auth_token) + + def test_validate_token_invalid(self): + # When the token is invalid the server typically returns a 404. + token_id = uuid.uuid4().hex + self.stub_url('GET', ['auth', 'tokens'], status_code=404) + + self.assertRaises(exceptions.NotFound, + self.client.tokens.get_token_data, token_id) + self.assertRaises(exceptions.NotFound, + self.client.tokens.validate, token_id) + + def test_validate_token_catalog(self): + # Can validate a token and a catalog is requested by default. + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_DEFAULT] + self.stub_url('GET', ['auth', 'tokens'], + headers={'X-Subject-Token': token_id, }, json=token_ref) + + token_data = self.client.tokens.get_token_data(token_id) + self.assertQueryStringIs() + self.assertIn('catalog', token_data['token']) + + access_info = self.client.tokens.validate(token_id) + + self.assertQueryStringIs() + self.assertTrue(access_info.has_service_catalog()) + + def test_validate_token_nocatalog(self): + # Can validate a token and request no catalog. + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_UNSCOPED] + self.stub_url('GET', ['auth', 'tokens'], + headers={'X-Subject-Token': token_id, }, json=token_ref) + + token_data = self.client.tokens.get_token_data(token_id) + self.assertQueryStringIs() + self.assertNotIn('catalog', token_data['token']) + + access_info = self.client.tokens.validate(token_id, + include_catalog=False) + + self.assertQueryStringIs('nocatalog') + self.assertFalse(access_info.has_service_catalog()) + + def test_validate_token_allow_expired(self): + token_id = uuid.uuid4().hex + token_ref = self.examples.TOKEN_RESPONSES[ + self.examples.v3_UUID_TOKEN_UNSCOPED] + self.stub_url('GET', ['auth', 'tokens'], + headers={'X-Subject-Token': token_id, }, json=token_ref) + + self.client.tokens.validate(token_id) + self.assertQueryStringIs() + + self.client.tokens.validate(token_id, allow_expired=True) + self.assertQueryStringIs('allow_expired=1') + + +def load_tests(loader, tests, pattern): + return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/v3/test_trusts.py b/keystoneclient/tests/unit/v3/test_trusts.py new file mode 100644 index 000000000..1c74ac9b9 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_trusts.py @@ -0,0 +1,128 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uuid + +from oslo_utils import timeutils + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib import trusts + + +class TrustTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(TrustTests, self).setUp() + self.key = 'trust' + self.collection_key = 'trusts' + self.model = trusts.Trust + self.manager = self.client.trusts + self.path_prefix = 'OS-TRUST' + + def new_ref(self, **kwargs): + kwargs = super(TrustTests, self).new_ref(**kwargs) + kwargs.setdefault('project_id', uuid.uuid4().hex) + return kwargs + + def test_create(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + super(TrustTests, self).test_create(ref=ref) + + def test_create_limited_uses(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + ref['remaining_uses'] = 5 + super(TrustTests, self).test_create(ref=ref) + + def test_create_roles(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + req_ref = ref.copy() + req_ref.pop('id') + + # Note the TrustManager takes a list of role_names, and converts + # internally to the slightly odd list-of-dict API format, so we + # have to pass the expected request data to allow correct stubbing + ref['role_names'] = ['atestrole'] + req_ref['roles'] = [{'name': 'atestrole'}] + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + + def test_create_role_id_and_names(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + req_ref = ref.copy() + req_ref.pop('id') + + # Note the TrustManager takes a list of role_names, and converts + # internally to the slightly odd list-of-dict API format, so we + # have to pass the expected request data to allow correct stubbing + ref['role_names'] = ['atestrole'] + ref['role_ids'] = [uuid.uuid4().hex] + req_ref['roles'] = [{'name': 'atestrole'}, {'id': ref['role_ids'][0]}] + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + + def test_create_expires(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + ref['expires_at'] = timeutils.parse_isotime( + '2013-03-04T12:00:01.000000Z') + req_ref = ref.copy() + req_ref.pop('id') + + # Note the TrustManager takes a datetime.datetime object for + # expires_at, and converts it internally into an iso format datestamp + req_ref['expires_at'] = '2013-03-04T12:00:01.000000Z' + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + + def test_create_imp(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = True + super(TrustTests, self).test_create(ref=ref) + + def test_create_roles_imp(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = True + req_ref = ref.copy() + req_ref.pop('id') + ref['role_names'] = ['atestrole'] + req_ref['roles'] = [{'name': 'atestrole'}] + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + + def test_list_filter_trustor(self): + expected_query = {'trustor_user_id': '12345'} + super(TrustTests, self).test_list(expected_query=expected_query, + trustor_user='12345') + + def test_list_filter_trustee(self): + expected_query = {'trustee_user_id': '12345'} + super(TrustTests, self).test_list(expected_query=expected_query, + trustee_user='12345') + + def test_update(self): + # Update not supported for the OS-TRUST API + self.assertRaises(exceptions.MethodNotImplemented, self.manager.update) diff --git a/keystoneclient/tests/unit/v3/test_users.py b/keystoneclient/tests/unit/v3/test_users.py new file mode 100644 index 000000000..a7f03f9ce --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_users.py @@ -0,0 +1,309 @@ +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock +import uuid + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3 import users + + +class UserTests(utils.ClientTestCase, utils.CrudTests): + def setUp(self): + super(UserTests, self).setUp() + self.key = 'user' + self.collection_key = 'users' + self.model = users.User + self.manager = self.client.users + + def new_ref(self, **kwargs): + kwargs = super(UserTests, self).new_ref(**kwargs) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('default_project_id', uuid.uuid4().hex) + return kwargs + + def test_add_user_to_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + self.stub_url('PUT', + ['groups', group_id, self.collection_key, ref['id']], + status_code=204) + + self.manager.add_to_group(user=ref['id'], group=group_id) + self.assertRaises(exceptions.ValidationError, + self.manager.remove_from_group, + user=ref['id'], + group=None) + + def test_list_users_in_group(self): + group_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + ['groups', group_id, self.collection_key], + entity=ref_list) + + returned_list = self.manager.list(group=group_id) + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + def test_check_user_in_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('HEAD', + ['groups', group_id, self.collection_key, ref['id']], + status_code=204) + + self.manager.check_in_group(user=ref['id'], group=group_id) + + self.assertRaises(exceptions.ValidationError, + self.manager.check_in_group, + user=ref['id'], + group=None) + + def test_remove_user_from_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url('DELETE', + ['groups', group_id, self.collection_key, ref['id']], + status_code=204) + + self.manager.remove_from_group(user=ref['id'], group=group_id) + self.assertRaises(exceptions.ValidationError, + self.manager.remove_from_group, + user=ref['id'], + group=None) + + def test_create_doesnt_log_password(self): + password = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_entity('POST', [self.collection_key], + status_code=201, entity=ref) + + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + param_ref['password'] = password + params = utils.parameterize(param_ref) + + self.manager.create(**params) + + self.assertNotIn(password, self.logger.output) + + def test_create_with_project(self): + # Can create a user with the deprecated project option rather than + # default_project_id. + self.deprecations.expect_deprecations() + ref = self.new_ref() + + self.stub_entity('POST', [self.collection_key], + status_code=201, entity=ref) + + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + # Use deprecated project_id rather than new default_project_id. + param_ref['project_id'] = param_ref.pop('default_project_id') + params = utils.parameterize(param_ref) + + returned = self.manager.create(**params) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + def test_create_with_project_and_default_project(self): + # Can create a user with the deprecated project and default_project_id. + # The backend call should only pass the default_project_id. + self.deprecations.expect_deprecations() + + ref = self.new_ref() + + self.stub_entity('POST', + [self.collection_key], + status_code=201, entity=ref) + + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + # Add the deprecated project_id in the call, the value will be ignored. + param_ref['project_id'] = 'project' + params = utils.parameterize(param_ref) + + returned = self.manager.create(**params) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + def test_update_doesnt_log_password(self): + password = uuid.uuid4().hex + ref = self.new_ref() + + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + self.stub_entity('PATCH', + [self.collection_key, ref['id']], + status_code=200, entity=ref) + + param_ref['password'] = password + params = utils.parameterize(param_ref) + + self.manager.update(ref['id'], **params) + + self.assertNotIn(password, self.logger.output) + + def test_update_with_project(self): + # Can update a user with the deprecated project option rather than + # default_project_id. + self.deprecations.expect_deprecations() + + ref = self.new_ref() + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + self.stub_entity('PATCH', + [self.collection_key, ref['id']], + status_code=200, entity=ref) + + # Use deprecated project_id rather than new default_project_id. + param_ref['project_id'] = param_ref.pop('default_project_id') + params = utils.parameterize(param_ref) + + returned = self.manager.update(ref['id'], **params) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + def test_update_with_project_and_default_project(self, ref=None): + self.deprecations.expect_deprecations() + + ref = self.new_ref() + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + self.stub_entity('PATCH', + [self.collection_key, ref['id']], + status_code=200, entity=ref) + + # Add the deprecated project_id in the call, the value will be ignored. + param_ref['project_id'] = 'project' + params = utils.parameterize(param_ref) + + returned = self.manager.update(ref['id'], **params) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + def test_update_password(self): + old_password = uuid.uuid4().hex + new_password = uuid.uuid4().hex + + self.stub_url('POST', + [self.collection_key, self.TEST_USER_ID, 'password']) + self.client.user_id = self.TEST_USER_ID + self.manager.update_password(old_password, new_password) + + exp_req_body = { + 'user': { + 'password': new_password, 'original_password': old_password + } + } + + self.assertEqual( + '%s/users/%s/password' % (self.TEST_URL, self.TEST_USER_ID), + self.requests_mock.last_request.url) + self.assertRequestBodyIs(json=exp_req_body) + self.assertNotIn(old_password, self.logger.output) + self.assertNotIn(new_password, self.logger.output) + + def test_update_password_with_no_hardcoded_endpoint_filter(self): + # test to ensure the 'endpoint_filter' parameter is not being + # passed from the manager. Endpoint filtering should be done at + # the Session, not the individual managers. + old_password = uuid.uuid4().hex + new_password = uuid.uuid4().hex + expected_params = {'user': {'password': new_password, + 'original_password': old_password}} + user_password_update_path = '/users/%s/password' % self.TEST_USER_ID + + self.client.user_id = self.TEST_USER_ID + # NOTE(gyee): user manager subclass keystoneclient.base.Manager + # and utilize the _update() method in the base class to interface + # with the client session to perform the update. In the case, we + # just need to make sure the 'endpoint_filter' parameter is not + # there. + with mock.patch('keystoneclient.base.Manager._update') as m: + self.manager.update_password(old_password, new_password) + m.assert_called_with(user_password_update_path, expected_params, + method='POST', log=False) + + def test_update_password_with_bad_inputs(self): + old_password = uuid.uuid4().hex + new_password = uuid.uuid4().hex + + # users can't unset their password + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + old_password, None) + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + old_password, '') + + # users can't start with empty passwords + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + None, new_password) + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + '', new_password) + + # this wouldn't result in any change anyway + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + None, None) + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + '', '') + password = uuid.uuid4().hex + self.assertRaises(exceptions.ValidationError, + self.manager.update_password, + password, password) diff --git a/keystoneclient/tests/unit/v3/utils.py b/keystoneclient/tests/unit/v3/utils.py new file mode 100644 index 000000000..cb3839a9c --- /dev/null +++ b/keystoneclient/tests/unit/v3/utils.py @@ -0,0 +1,396 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import requests +import uuid + +from urllib import parse as urlparse + +from keystoneauth1.identity import v3 +from keystoneauth1 import session +from keystoneclient.tests.unit import client_fixtures +from keystoneclient.tests.unit import utils +from keystoneclient.v3 import client + + +def parameterize(ref): + """Rewrite attributes to match the kwarg naming convention in client. + + >>> parameterize({'project_id': 0}) + {'project': 0} + + """ + params = ref.copy() + for key in ref: + if key[-3:] == '_id': + params.setdefault(key[:-3], params.pop(key)) + return params + + +class UnauthenticatedTestCase(utils.TestCase): + """Class used as base for unauthenticated calls.""" + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') + + +class TestCase(UnauthenticatedTestCase): + + TEST_ADMIN_IDENTITY_ENDPOINT = "http://127.0.0.1:35357/v3" + TEST_PUBLIC_IDENTITY_ENDPOINT = "http://127.0.0.1:5000/v3" + + TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "url": "http://cdn.admin-nets.local:8774/v1.0/", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://127.0.0.1:8774/v1.0", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "interface": "admin" + }], + "type": "nova_compat" + }, { + "endpoints": [{ + "url": "http://nova/novapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://nova/novapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://nova/novapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "compute" + }, { + "endpoints": [{ + "url": "http://glance/glanceapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://glance/glanceapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://glance/glanceapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "image", + "name": "glance" + }, { + "endpoints": [{ + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne", + "interface": "internal" + }, { + "url": TEST_ADMIN_IDENTITY_ENDPOINT, + "region": "RegionOne", + "interface": "admin" + }], + "type": "identity" + }, { + "endpoints": [{ + "url": "http://swift/swiftapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://swift/swiftapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://swift/swiftapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "object-store" + }] + + def stub_auth(self, subject_token=None, **kwargs): + + if not subject_token: + subject_token = self.TEST_TOKEN + + try: + response_list = kwargs['response_list'] + except KeyError: + headers = kwargs.setdefault('headers', {}) + headers['X-Subject-Token'] = subject_token + else: + for resp in response_list: + headers = resp.setdefault('headers', {}) + headers['X-Subject-Token'] = subject_token + + self.stub_url('POST', ['auth', 'tokens'], **kwargs) + + +class ClientTestCase(utils.ClientTestCaseMixin, TestCase): + + ORIGINAL_CLIENT_TYPE = 'original' + KSC_SESSION_CLIENT_TYPE = 'ksc-session' + KSA_SESSION_CLIENT_TYPE = 'ksa-session' + + scenarios = [ + ( + ORIGINAL_CLIENT_TYPE, { + 'client_fixture_class': client_fixtures.OriginalV3, + 'client_type': ORIGINAL_CLIENT_TYPE + } + ), + ( + KSC_SESSION_CLIENT_TYPE, { + 'client_fixture_class': client_fixtures.KscSessionV3, + 'client_type': KSC_SESSION_CLIENT_TYPE + } + ), + ( + KSA_SESSION_CLIENT_TYPE, { + 'client_fixture_class': client_fixtures.KsaSessionV3, + 'client_type': KSA_SESSION_CLIENT_TYPE + } + ) + + ] + + @property + def is_original_client(self): + return self.client_type == self.ORIGINAL_CLIENT_TYPE + + @property + def is_session_client(self): + return self.client_type in (self.KSC_SESSION_CLIENT_TYPE, + self.KSA_SESSION_CLIENT_TYPE) + + +class CrudTests(object): + key = None + collection_key = None + model = None + manager = None + path_prefix = None + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault(uuid.uuid4().hex, uuid.uuid4().hex) + return kwargs + + def encode(self, entity): + if isinstance(entity, dict): + return {self.key: entity} + if isinstance(entity, list): + return {self.collection_key: entity} + raise NotImplementedError('Are you sure you want to encode that?') + + def stub_entity(self, method, parts=None, entity=None, id=None, **kwargs): + if entity: + entity = self.encode(entity) + kwargs['json'] = entity + + if not parts: + parts = [self.collection_key] + + if self.path_prefix: + parts.insert(0, self.path_prefix) + + if id: + if not parts: + parts = [] + + parts.append(id) + + self.stub_url(method, parts=parts, **kwargs) + + def assertEntityRequestBodyIs(self, entity): + self.assertRequestBodyIs(json=self.encode(entity)) + + def test_create(self, ref=None, req_ref=None): + deprecations = self.useFixture(client_fixtures.Deprecations()) + deprecations.expect_deprecations() + ref = ref or self.new_ref() + manager_ref = ref.copy() + manager_ref.pop('id') + + # req_ref argument allows you to specify a different + # signature for the request when the manager does some + # conversion before doing the request (e.g. converting + # from datetime object to timestamp string) + if req_ref: + req_ref = req_ref.copy() + else: + req_ref = ref.copy() + req_ref.pop('id') + + self.stub_entity('POST', entity=req_ref, status_code=201) + + returned = self.manager.create(**parameterize(manager_ref)) + self.assertIsInstance(returned, self.model) + for attr in req_ref: + self.assertEqual( + getattr(returned, attr), + req_ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + # The entity created here may be used in other test cases + return returned + + def test_get(self, ref=None): + ref = ref or self.new_ref() + + self.stub_entity('GET', id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id']) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + def _get_expected_path(self, expected_path=None): + if not expected_path: + if self.path_prefix: + expected_path = 'v3/%s/%s' % (self.path_prefix, + self.collection_key) + else: + expected_path = 'v3/%s' % self.collection_key + + return expected_path + + def test_list_by_id(self, ref=None, **filter_kwargs): + """Test ``entities.list(id=x)`` being rewritten as ``GET /v3/entities/x``. # noqa + + This tests an edge case of each manager's list() implementation, to + ensure that it "does the right thing" when users call ``.list()`` + when they should have used ``.get()``. + + """ + if 'id' not in filter_kwargs: + ref = ref or self.new_ref() + filter_kwargs['id'] = ref['id'] + + self.assertRaises(TypeError, self.manager.list, **filter_kwargs) + + def test_list(self, ref_list=None, expected_path=None, + expected_query=None, **filter_kwargs): + ref_list = ref_list or [self.new_ref(), self.new_ref()] + expected_path = self._get_expected_path(expected_path) + + self.requests_mock.get(urlparse.urljoin(self.TEST_URL, expected_path), + json=self.encode(ref_list)) + + returned_list = self.manager.list(**filter_kwargs) + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + qs_args = self.requests_mock.last_request.qs + qs_args_expected = expected_query or filter_kwargs + for key, value in qs_args_expected.items(): + self.assertIn(key, qs_args) + # The querystring value is a list. Note we convert the value to a + # string and lower, as the query string is always a string and the + # filter_kwargs may contain non-string values, for example a + # boolean, causing the comaprison to fail. + self.assertIn(str(value).lower(), qs_args[key]) + + # Also check that no query string args exist which are not expected + for key in qs_args: + self.assertIn(key, qs_args_expected) + + def test_list_params(self): + ref_list = [self.new_ref()] + filter_kwargs = {uuid.uuid4().hex: uuid.uuid4().hex} + expected_path = self._get_expected_path() + + self.requests_mock.get(urlparse.urljoin(self.TEST_URL, expected_path), + json=self.encode(ref_list)) + + self.manager.list(**filter_kwargs) + self.assertQueryStringContains(**filter_kwargs) + + def test_find(self, ref=None): + ref = ref or self.new_ref() + ref_list = [ref] + + self.stub_entity('GET', entity=ref_list) + + returned = self.manager.find(name=getattr(ref, 'name', None)) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + if hasattr(ref, 'name'): + self.assertQueryStringIs('name=%s' % ref['name']) + else: + self.assertQueryStringIs('') + + def test_update(self, ref=None, req_ref=None): + deprecations = self.useFixture(client_fixtures.Deprecations()) + deprecations.expect_deprecations() + ref = ref or self.new_ref() + + self.stub_entity('PATCH', id=ref['id'], entity=ref) + + # req_ref argument allows you to specify a different + # signature for the request when the manager does some + # conversion before doing the request (e.g. converting + # from datetime object to timestamp string) + if req_ref: + req_ref = req_ref.copy() + else: + req_ref = ref.copy() + req_ref.pop('id') + + returned = self.manager.update(ref['id'], **parameterize(req_ref)) + self.assertIsInstance(returned, self.model) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + def test_delete(self, ref=None): + ref = ref or self.new_ref() + + self.stub_entity('DELETE', id=ref['id'], status_code=204) + self.manager.delete(ref['id']) + + +class TestRequestId(TestCase): + resp = requests.Response() + TEST_REQUEST_ID = uuid.uuid4().hex + resp.headers['x-openstack-request-id'] = TEST_REQUEST_ID + + def setUp(self): + super(TestRequestId, self).setUp() + auth = v3.Token(auth_url='http://127.0.0.1:5000', + token=self.TEST_TOKEN) + session_ = session.Session(auth=auth) + self.client = client.Client(session=session_, + include_metadata='True')._adapter diff --git a/keystoneclient/utils.py b/keystoneclient/utils.py index eae149e37..1c31f2bd3 100644 --- a/keystoneclient/utils.py +++ b/keystoneclient/utils.py @@ -1,125 +1,122 @@ -import uuid +# 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 getpass import hashlib +import sys -import prettytable +from keystoneauth1 import exceptions as ksa_exceptions +from oslo_utils import timeutils -from keystoneclient import exceptions - - -# Decorator for cli-args -def arg(*args, **kwargs): - def _decorator(func): - # Because of the sematics of decorator composition if we just append - # to the options list positional options will appear to be backwards. - func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) - return func - return _decorator - - -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) - - -def print_list(objs, fields, formatters={}, order_by=None): - pt = prettytable.PrettyTable([f for f in fields], caching=False) - pt.aligns = ['l' for f in fields] - - for o in objs: - row = [] - for field in fields: - if field in formatters: - row.append(formatters[field](o)) - else: - field_name = field.lower().replace(' ', '_') - data = getattr(o, field_name, '') - if data is None: - data = '' - row.append(data) - pt.add_row(row) - - if order_by is None: - order_by = fields[0] - print pt.get_string(sortby=order_by) - - -def _word_wrap(string, max_length=0): - """wrap long strings to be no longer then max_length""" - if max_length <= 0: - return string - return '\n'.join([string[i:i + max_length] for i in - range(0, len(string), max_length)]) - - -def print_dict(d, wrap=0): - """pretty table prints dictionaries. - - Wrap values to max_length wrap if wrap>0 - """ - pt = prettytable.PrettyTable(['Property', 'Value'], caching=False) - pt.aligns = ['l', 'l'] - for (prop, value) in d.iteritems(): - if value is None: - value = '' - value = _word_wrap(value, max_length=wrap) - pt.add_row([prop, value]) - print pt.get_string(sortby='Property') +from keystoneclient import exceptions as ksc_exceptions def find_resource(manager, name_or_id): """Helper for the _find_* methods.""" - # first try to get entity as integer id - try: - if isinstance(name_or_id, int) or name_or_id.isdigit(): - return manager.get(int(name_or_id)) - except exceptions.NotFound: - pass - - # now try to get entity as uuid + # first try the entity as a string try: - uuid.UUID(str(name_or_id)) return manager.get(name_or_id) - except (ValueError, exceptions.NotFound): + except (ksa_exceptions.NotFound): # nosec(cjschaef): try to find + # 'name_or_id' as a bytes instead pass # finally try to find entity by name try: + if isinstance(name_or_id, bytes): + name_or_id = name_or_id.decode('utf-8', 'strict') return manager.find(name=name_or_id) - except exceptions.NotFound: + except ksa_exceptions.NotFound: msg = ("No %s with a name or ID of '%s' exists." % (manager.resource_class.__name__.lower(), name_or_id)) - raise exceptions.CommandError(msg) - + raise ksc_exceptions.CommandError(msg) + except ksc_exceptions.NoUniqueMatch: + msg = ("Multiple %s matches found for '%s', use an ID to be more" + " specific." % (manager.resource_class.__name__.lower(), + name_or_id)) + raise ksc_exceptions.CommandError(msg) -def unauthenticated(f): - """Adds 'unauthenticated' attribute to decorated function. - Usage:: +def hash_signed_token(signed_text, mode='md5'): + hash_ = hashlib.new(mode) + hash_.update(signed_text) + return hash_.hexdigest() - @unauthenticated - def mymethod(f): - ... - """ - f.unauthenticated = True - return f +def prompt_user_password(): + """Prompt user for a password. -def isunauthenticated(f): - """ - Checks to see if the function is marked as not requiring authentication - with the @unauthenticated decorator. Returns True if decorator is - set to True, False otherwise. + Prompt for a password if stdin is a tty. """ - return getattr(f, 'unauthenticated', False) + password = None + # If stdin is a tty, try prompting for the password + if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): + # Check for Ctl-D + try: + password = getpass.getpass('Password: ') + except EOFError: # nosec(cjschaef): return password, which is None if + # password was not found + pass -def string_to_bool(arg): - if isinstance(arg, bool): - return arg + return password - return arg.strip().lower() in ('t', 'true', 'yes', '1') +def prompt_for_password(): + """Prompt user for password if not provided. -def hash_signed_token(signed_text): - hash_ = hashlib.md5() - hash_.update(signed_text) - return hash_.hexdigest() + Prompt is used so the password doesn't show up in the + bash history. + """ + if not (hasattr(sys.stdin, 'isatty') and sys.stdin.isatty()): + # nothing to do + return + + while True: + try: + new_passwd = getpass.getpass('New Password: ') + rep_passwd = getpass.getpass('Repeat New Password: ') + if new_passwd == rep_passwd: + return new_passwd + except EOFError: + return + + +_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f' +_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' + + +def isotime(at=None, subsecond=False): + """Stringify time in ISO 8601 format.""" + # Python provides a similar instance method for datetime.datetime objects + # called isoformat(). The format of the strings generated by isoformat() + # have a couple of problems: + # 1) The strings generated by isotime are used in tokens and other public + # APIs that we can't change without a deprecation period. The strings + # generated by isoformat are not the same format, so we can't just + # change to it. + # 2) The strings generated by isoformat do not include the microseconds if + # the value happens to be 0. This will likely show up as random failures + # as parsers may be written to always expect microseconds, and it will + # parse correctly most of the time. + if not at: + at = timeutils.utcnow() + st = at.strftime(_ISO8601_TIME_FORMAT + if not subsecond + else _ISO8601_TIME_FORMAT_SUBSECOND) + tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' + st += ('Z' if (tz == 'UTC' or tz == 'UTC+00:00') else tz) + return st + + +def strtime(at=None): + at = at or timeutils.utcnow() + return at.strftime(timeutils.PERFECT_TIME_FORMAT) diff --git a/keystoneclient/v2_0/__init__.py b/keystoneclient/v2_0/__init__.py index 44ad28e7c..23382fea1 100644 --- a/keystoneclient/v2_0/__init__.py +++ b/keystoneclient/v2_0/__init__.py @@ -1 +1,6 @@ -from keystoneclient.v2_0.client import Client +from keystoneclient.v2_0.client import Client # noqa + + +__all__ = ( + 'client', +) diff --git a/keystoneclient/v2_0/certificates.py b/keystoneclient/v2_0/certificates.py new file mode 100644 index 000000000..2c69dfb3d --- /dev/null +++ b/keystoneclient/v2_0/certificates.py @@ -0,0 +1,40 @@ +# Copyright 2014 IBM Corp. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class CertificatesManager(object): + """Manager for certificates.""" + + def __init__(self, client): + self._client = client + + def get_ca_certificate(self): + """Get CA certificate. + + :returns: PEM-formatted string. + :rtype: str + + """ + resp, body = self._client.get('/certificates/ca', authenticated=False) + return resp.text + + def get_signing_certificate(self): + """Get signing certificate. + + :returns: PEM-formatted string. + :rtype: str + + """ + resp, body = self._client.get('/certificates/signing', + authenticated=False) + return resp.text diff --git a/keystoneclient/v2_0/client.py b/keystoneclient/v2_0/client.py index e98f7a2c4..904f76935 100644 --- a/keystoneclient/v2_0/client.py +++ b/keystoneclient/v2_0/client.py @@ -12,14 +12,18 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + import logging +import warnings -from keystoneclient import access -from keystoneclient import client +from keystoneclient.auth.identity import v2 as v2_auth from keystoneclient import exceptions -from keystoneclient import service_catalog +from keystoneclient import httpclient +from keystoneclient.i18n import _ +from keystoneclient.v2_0 import certificates from keystoneclient.v2_0 import ec2 from keystoneclient.v2_0 import endpoints +from keystoneclient.v2_0 import extensions from keystoneclient.v2_0 import roles from keystoneclient.v2_0 import services from keystoneclient.v2_0 import tenants @@ -30,7 +34,7 @@ _logger = logging.getLogger(__name__) -class Client(client.HTTPClient): +class Client(httpclient.HTTPClient): """Client for the OpenStack Keystone v2.0 API. :param string username: Username for authentication. (optional) @@ -50,16 +54,21 @@ class Client(client.HTTPClient): :param string original_ip: The original IP of the requesting user which will be sent to Keystone in a 'Forwarded' header. (optional) - :param string cert: If provided, used as a local certificate to communicate - with the keystone endpoint. If provided, requires the - additional parameter key. (optional) - :param string key: The key associated with the certificate for secure - keystone communication. (optional) - :param string cacert: the ca-certs to verify the secure communications - with keystone. (optional) - :param boolean insecure: If using an SSL endpoint, allows for the certicate - to be unsigned - does not verify the certificate - chain. default: False (optional) + :param string cert: Path to the Privacy Enhanced Mail (PEM) file which + contains the corresponding X.509 client certificate + needed to established two-way SSL connection with + the identity service. (optional) + :param string key: Path to the Privacy Enhanced Mail (PEM) file which + contains the unencrypted client private key needed + to established two-way SSL connection with the + identity service. (optional) + :param string cacert: Path to the Privacy Enhanced Mail (PEM) file which + contains the trusted authority X.509 certificates + needed to established SSL connection with the + identity service. (optional) + :param boolean insecure: Does not perform X.509 certificate validation + when establishing SSL connection with identity + service. default: False (optional) :param dict auth_ref: To allow for consumers of the client to manage their own caching strategy, you may initialize a client with a previously captured auth_reference (token) @@ -68,9 +77,14 @@ class Client(client.HTTPClient): .. warning:: - If debug is enabled, it may show passwords in plain text as a part of its - output. + If debug is enabled, it may show passwords in plain text as a part of + its output. + + .. warning:: + Constructing an instance of this class without a session is + deprecated as of the 1.7.0 release and will be removed in the + 2.0.0 release. The client can be created and used like a user or in a strictly bootstrap mode. Normal operation expects a username, password, auth_url, @@ -79,11 +93,15 @@ class Client(client.HTTPClient): Example:: + >>> from keystoneauth1.identity import v2 + >>> from keystoneauth1 import session >>> from keystoneclient.v2_0 import client - >>> keystone = client.Client(username=USER, - password=PASS, - tenant_name=TENANT_NAME, - auth_url=KEYSTONE_URL) + >>> auth = v2.Password(auth_url=KEYSTONE_URL, + ... username=USER, + ... password=PASS, + ... tenant_name=TENANT_NAME) + >>> sess = session.Session(auth=auth) + >>> keystone = client.Client(session=sess) >>> keystone.tenants.list() ... >>> user = keystone.users.get(USER_ID) @@ -94,11 +112,15 @@ class Client(client.HTTPClient): returns as a dictionary-like-object so that you can export and cache it, re-using it when initiating another client:: + >>> from keystoneauth1.identity import v2 + >>> from keystoneauth1 import session >>> from keystoneclient.v2_0 import client - >>> keystone = client.Client(username=USER, - password=PASS, - tenant_name=TENANT_NAME, - auth_url=KEYSTONE_URL) + >>> auth = v2.Password(auth_url=KEYSTONE_URL, + ... username=USER, + ... password=PASS, + ... tenant_name=TENANT_NAME) + >>> sess = session.Session(auth=auth) + >>> keystone = client.Client(session=sess) >>> auth_ref = keystone.auth_ref >>> # pickle or whatever you like here >>> new_client = client.Client(auth_ref=auth_ref) @@ -110,109 +132,89 @@ class Client(client.HTTPClient): Example:: + >>> from keystoneauth1.identity import v2 + >>> from keystoneauth1 import session >>> from keystoneclient.v2_0 import client - >>> admin_client = client.Client( - token='12345secret7890', - endpoint='http://localhost:35357/v2.0') + >>> auth = v2.Token(auth_url='http://localhost:35357/v2.0', + ... token='12345secret7890') + >>> sess = session.Session(auth=auth) + >>> keystone = client.Client(session=sess) >>> keystone.tenants.list() """ + version = 'v2.0' + def __init__(self, **kwargs): - """ Initialize a new client for the Keystone v2.0 API. """ - super(Client, self).__init__(**kwargs) - self.endpoints = endpoints.EndpointManager(self) - self.roles = roles.RoleManager(self) - self.services = services.ServiceManager(self) - self.tenants = tenants.TenantManager(self) - self.tokens = tokens.TokenManager(self) - self.users = users.UserManager(self) + """Initialize a new client for the Keystone v2.0 API.""" + if not kwargs.get('session'): + warnings.warn( + 'Constructing an instance of the ' + 'keystoneclient.v2_0.client.Client class without a session is ' + 'deprecated as of the 1.7.0 release and may be removed in ' + 'the 2.0.0 release.', DeprecationWarning) - # extensions - self.ec2 = ec2.CredentialsManager(self) + super(Client, self).__init__(**kwargs) - if self.management_url is None: - self.authenticate() + self.certificates = certificates.CertificatesManager(self._adapter) + self.endpoints = endpoints.EndpointManager(self._adapter) + self.extensions = extensions.ExtensionManager(self._adapter) + self.roles = roles.RoleManager(self._adapter) + self.services = services.ServiceManager(self._adapter) + self.tokens = tokens.TokenManager(self._adapter) + self.users = users.UserManager(self._adapter, self.roles) - #TODO(heckj): move to a method on auth_ref - def has_service_catalog(self): - """Returns True if this client provides a service catalog.""" - return hasattr(self, 'service_catalog') + self.tenants = tenants.TenantManager(self._adapter, + self.roles, self.users) - def process_token(self): - """ Extract and process information from the new auth_ref. + # extensions + self.ec2 = ec2.CredentialsManager(self._adapter) - And set the relevant authentication information. - """ - # if we got a response without a service catalog, set the local - # list of tenants for introspection, and leave to client user - # to determine what to do. Otherwise, load up the service catalog - self.auth_token = self.auth_ref.auth_token - if self.auth_ref.scoped: - if self.management_url is None and self.auth_ref.management_url: - self.management_url = self.auth_ref.management_url[0] - self.tenant_name = self.auth_ref.tenant_name - self.tenant_id = self.auth_ref.tenant_id - self.user_id = self.auth_ref.user_id - self._extract_service_catalog(self.auth_url, self.auth_ref) + # DEPRECATED: if session is passed then we go to the new behaviour of + # authenticating on the first required call. + if not kwargs.get('session') and self.management_url is None: + self.authenticate() def get_raw_token_from_identity_service(self, auth_url, username=None, password=None, tenant_name=None, - tenant_id=None, token=None): - """ Authenticate against the Keystone API. - - :returns: ``raw token`` if authentication was successful. - :raises: AuthorizationFailure if unable to authenticate or validate - the existing authorization token - :raises: ValueError if insufficient parameters are used. - + tenant_id=None, token=None, + project_name=None, project_id=None, + trust_id=None, + **kwargs): + """Authenticate against the v2 Identity API. + + If a token is provided it will be used in preference over username and + password. + + :returns: access.AccessInfo if authentication was successful. + :raises keystoneclient.exceptions.AuthorizationFailure: if unable to + authenticate or validate the existing authorization token """ try: - return self._base_authN(auth_url, - username=username, - tenant_id=tenant_id, - tenant_name=tenant_name, - password=password, - token=token) + if auth_url is None: + raise ValueError(_("Cannot authenticate without an auth_url")) + + new_kwargs = {'trust_id': trust_id, + 'tenant_id': project_id or tenant_id, + 'tenant_name': project_name or tenant_name} + + if token: + plugin = v2_auth.Token(auth_url, token, **new_kwargs) + elif username and password: + plugin = v2_auth.Password(auth_url, username, password, + **new_kwargs) + else: + msg = _('A username and password or token is required.') + raise exceptions.AuthorizationFailure(msg) + + return plugin.get_auth_ref(self.session) except (exceptions.AuthorizationFailure, exceptions.Unauthorized): _logger.debug("Authorization Failed.") raise + except exceptions.EndpointNotFound: + msg = ( + _('There was no suitable authentication url for this request')) + raise exceptions.AuthorizationFailure(msg) except Exception as e: - raise exceptions.AuthorizationFailure("Authorization Failed: " - "%s" % e) - - def _base_authN(self, auth_url, username=None, password=None, - tenant_name=None, tenant_id=None, token=None): - """ Takes a username, password, and optionally a tenant_id or - tenant_name to get an authentication token from keystone. - May also take a token and a tenant_id to re-scope a token - to a tenant.""" - headers = {} - url = auth_url + "/tokens" - if token: - headers['X-Auth-Token'] = token - params = {"auth": {"token": {"id": token}}} - elif username and password: - params = {"auth": {"passwordCredentials": {"username": username, - "password": password}}} - else: - raise ValueError('A username and password or token is required.') - if tenant_id: - params['auth']['tenantId'] = tenant_id - elif tenant_name: - params['auth']['tenantName'] = tenant_name - resp, body = self.request(url, 'POST', body=params, headers=headers) - return body['access'] - - # TODO(heckj): remove entirely in favor of access.AccessInfo and - # associated methods - def _extract_service_catalog(self, url, body): - """ Set the client's service catalog from the response data. """ - self.service_catalog = service_catalog.ServiceCatalog(body) - try: - sc = self.service_catalog.get_token() - # Save these since we have them and they'll be useful later - self.auth_tenant_id = sc.get('tenant_id') - self.auth_user_id = sc.get('user_id') - except KeyError: - raise exceptions.AuthorizationFailure() + raise exceptions.AuthorizationFailure( + _("Authorization Failed: %s") % e) diff --git a/keystoneclient/v2_0/ec2.py b/keystoneclient/v2_0/ec2.py index e0f48829c..0abe98b07 100644 --- a/keystoneclient/v2_0/ec2.py +++ b/keystoneclient/v2_0/ec2.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -18,6 +18,7 @@ class EC2(base.Resource): def __repr__(self): + """Return string representation of EC2 resource information.""" return "" % self._info def delete(self): @@ -28,35 +29,32 @@ class CredentialsManager(base.ManagerWithFind): resource_class = EC2 def create(self, user_id, tenant_id): - """ - Create a new access/secret pair for the user/tenant pair + """Create a new access/secret pair for the user/tenant pair. + :rtype: object of type :class:`EC2` """ - params = {'tenant_id': tenant_id} - return self._create('/users/%s/credentials/OS-EC2' % user_id, - params, "credential") + return self._post('/users/%s/credentials/OS-EC2' % user_id, + params, "credential") def list(self, user_id): - """ - Get a list of access/secret pairs for a user_id + """Get a list of access/secret pairs for a user_id. + :rtype: list of :class:`EC2` """ return self._list("/users/%s/credentials/OS-EC2" % user_id, "credentials") def get(self, user_id, access): - """ - Get the access/secret pair for a given access key + """Get the access/secret pair for a given access key. + :rtype: object of type :class:`EC2` """ return self._get("/users/%s/credentials/OS-EC2/%s" % (user_id, base.getid(access)), "credential") def delete(self, user_id, access): - """ - Delete an access/secret pair for a user - """ + """Delete an access/secret pair for a user.""" return self._delete("/users/%s/credentials/OS-EC2/%s" % (user_id, base.getid(access))) diff --git a/keystoneclient/v2_0/endpoints.py b/keystoneclient/v2_0/endpoints.py index eaa1caf8f..13ac399d1 100644 --- a/keystoneclient/v2_0/endpoints.py +++ b/keystoneclient/v2_0/endpoints.py @@ -17,29 +17,32 @@ class Endpoint(base.Resource): - """Represents a Keystone endpoint""" + """Represents a Keystone endpoint.""" + def __repr__(self): + """Return string representation of endpoint resource information.""" return "" % self._info class EndpointManager(base.ManagerWithFind): - """Manager class for manipulating Keystone endpoints""" + """Manager class for manipulating Keystone endpoints.""" resource_class = Endpoint def list(self): - """List all available endpoints""" + """List all available endpoints.""" return self._list('/endpoints', 'endpoints') - def create(self, region, service_id, publicurl, adminurl, internalurl): - """Create a new endpoint""" + def create(self, region, service_id, publicurl, adminurl=None, + internalurl=None): + """Create a new endpoint.""" body = {'endpoint': {'region': region, 'service_id': service_id, 'publicurl': publicurl, 'adminurl': adminurl, 'internalurl': internalurl}} - return self._create('/endpoints', body, 'endpoint') + return self._post('/endpoints', body, 'endpoint') def delete(self, id): - """Delete an endpoint""" + """Delete an endpoint.""" return self._delete('/endpoints/%s' % id) diff --git a/keystoneclient/v2_0/extensions.py b/keystoneclient/v2_0/extensions.py new file mode 100644 index 000000000..f1c402700 --- /dev/null +++ b/keystoneclient/v2_0/extensions.py @@ -0,0 +1,31 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Extension(base.Resource): + """Represents an Identity API extension.""" + + def __repr__(self): + """Return string representation of extension resource information.""" + return "" % self._info + + +class ExtensionManager(base.ManagerWithFind): + """Manager class for listing Identity API extensions.""" + + resource_class = Extension + + def list(self): + """List all available extensions.""" + return self._list('/extensions', 'extensions') diff --git a/keystoneclient/v2_0/roles.py b/keystoneclient/v2_0/roles.py index 71f75ad94..b91808446 100644 --- a/keystoneclient/v2_0/roles.py +++ b/keystoneclient/v2_0/roles.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -18,8 +18,10 @@ class Role(base.Resource): - """Represents a Keystone role""" + """Represents a Keystone role.""" + def __repr__(self): + """Return string representation of role resource information.""" return "" % self._info def delete(self): @@ -27,29 +29,24 @@ def delete(self): class RoleManager(base.ManagerWithFind): - """Manager class for manipulating Keystone roles""" + """Manager class for manipulating Keystone roles.""" + resource_class = Role def get(self, role): return self._get("/OS-KSADM/roles/%s" % base.getid(role), "role") def create(self, name): - """ - Create a role. - """ + """Create a role.""" params = {"role": {"name": name}} - return self._create('/OS-KSADM/roles', params, "role") + return self._post('/OS-KSADM/roles', params, "role") def delete(self, role): - """ - Delete a role. - """ + """Delete a role.""" return self._delete("/OS-KSADM/roles/%s" % base.getid(role)) def list(self): - """ - List all available roles. - """ + """List all available roles.""" return self._list("/OS-KSADM/roles", "roles") def roles_for_user(self, user, tenant=None): @@ -62,7 +59,7 @@ def roles_for_user(self, user, tenant=None): return self._list("/users/%s/roles" % user_id, "roles") def add_user_role(self, user, role, tenant=None): - """ Adds a role to a user. + """Add a role to a user. If tenant is specified, the role is added just for that tenant, otherwise the role is added globally. @@ -78,7 +75,7 @@ def add_user_role(self, user, role, tenant=None): return self._update(route % (user_id, role_id), None, "roles") def remove_user_role(self, user, role, tenant=None): - """ Removes a role from a user. + """Remove a role from a user. If tenant is specified, the role is removed just for that tenant, otherwise the role is removed from the user's global roles. diff --git a/keystoneclient/v2_0/services.py b/keystoneclient/v2_0/services.py index 04dd90f0e..8f20b4daa 100644 --- a/keystoneclient/v2_0/services.py +++ b/keystoneclient/v2_0/services.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -18,30 +18,33 @@ class Service(base.Resource): - """Represents a Keystone service""" + """Represents a Keystone service.""" + def __repr__(self): + """Return string representation of service resource information.""" return "" % self._info class ServiceManager(base.ManagerWithFind): - """Manager class for manipulating Keystone services""" + """Manager class for manipulating Keystone services.""" + resource_class = Service def list(self): - """List available services""" + """List available services.""" return self._list("/OS-KSADM/services", "OS-KSADM:services") def get(self, id): - """Retrieve a service by id""" + """Retrieve a service by id.""" return self._get("/OS-KSADM/services/%s" % id, "OS-KSADM:service") - def create(self, name, service_type, description): - """Create a new service""" + def create(self, name, service_type, description=None): + """Create a new service.""" body = {"OS-KSADM:service": {'name': name, 'type': service_type, 'description': description}} - return self._create("/OS-KSADM/services", body, "OS-KSADM:service") + return self._post("/OS-KSADM/services", body, "OS-KSADM:service") def delete(self, id): - """Delete a service""" + """Delete a service.""" return self._delete("/OS-KSADM/services/%s" % id) diff --git a/keystoneclient/v2_0/shell.py b/keystoneclient/v2_0/shell.py deleted file mode 100755 index 3eb86084d..000000000 --- a/keystoneclient/v2_0/shell.py +++ /dev/null @@ -1,468 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. -# 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 argparse -import getpass - -from keystoneclient.v2_0 import client -from keystoneclient import utils - - -CLIENT_CLASS = client.Client - - -def require_service_catalog(f): - msg = ('Configuration error: Client configured to run without a service ' - 'catalog. Run the client using --os-auth-url or OS_AUTH_URL, ' - 'instead of --os-endpoint or OS_SERVICE_ENDPOINT, for example.') - - def wrapped(kc, args): - if not kc.has_service_catalog(): - raise Exception(msg) - return f(kc, args) - - return wrapped - - -@utils.arg('--tenant-id', metavar='', - help='Tenant ID; lists all users if not specified') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -def do_user_list(kc, args): - """List users""" - users = kc.users.list(tenant_id=args.tenant_id) - utils.print_list(users, ['id', 'name', 'enabled', 'email'], - order_by='name') - - -@utils.arg('id', metavar='', help='User ID to display') -def do_user_get(kc, args): - """Display user details.""" - user = kc.users.get(args.id) - utils.print_dict(user._info) - - -@utils.arg('--name', metavar='', required=True, - help='New user name (must be unique)') -@utils.arg('--tenant-id', metavar='', - help='New user default tenant') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -@utils.arg('--pass', metavar='', dest='passwd', - help='New user password') -@utils.arg('--email', metavar='', - help='New user email address') -@utils.arg('--enabled', metavar='', default=True, - help='Initial user enabled status (default true)') -def do_user_create(kc, args): - """Create new user""" - user = kc.users.create(args.name, args.passwd, args.email, - tenant_id=args.tenant_id, - enabled=utils.string_to_bool(args.enabled)) - utils.print_dict(user._info) - - -@utils.arg('--name', metavar='', - help='Desired new user name') -@utils.arg('--email', metavar='', - help='Desired new email address') -@utils.arg('--enabled', metavar='', - help='Enable or disable user') -@utils.arg('id', metavar='', help='User ID to update') -def do_user_update(kc, args): - """Update user's name, email, and enabled status""" - kwargs = {} - if args.name: - kwargs['name'] = args.name - if args.email: - kwargs['email'] = args.email - if args.enabled: - kwargs['enabled'] = utils.string_to_bool(args.enabled) - - if not len(kwargs): - print "User not updated, no arguments present." - return - - try: - kc.users.update(args.id, **kwargs) - print 'User has been updated.' - except Exception as e: - print 'Unable to update user: %s' % e - - -@utils.arg('--pass', metavar='', dest='passwd', required=True, - help='Desired new password') -@utils.arg('id', metavar='', help='User ID to update') -def do_user_password_update(kc, args): - """Update user password""" - kc.users.update_password(args.id, args.passwd) - - -@utils.arg('--current-password', metavar='', - dest='currentpasswd', required=False, help='Current password, ' - 'Defaults to the password as set by --os-password or ' - 'OS_PASSWORD') -@utils.arg('--new-password ', metavar='', dest='newpasswd', - required=False, help='Desired new password') -def do_password_update(kc, args): - """Update own password""" - - # we are prompting for these passwords if they are not passed in - # this gives users the option not to have their password - # appear in bash history etc.. - currentpasswd = args.os_password - if args.currentpasswd is not None: - currentpasswd = args.currentpasswd - if currentpasswd is None: - currentpasswd = getpass.getpass('Current Password: ') - - newpasswd = args.newpasswd - while newpasswd is None: - passwd1 = getpass.getpass('New Password: ') - passwd2 = getpass.getpass('Repeat New Password: ') - if passwd1 == passwd2: - newpasswd = passwd1 - - kc.users.update_own_password(currentpasswd, newpasswd) - - if args.os_password != newpasswd: - print "You should update the password you are using to authenticate "\ - "to match your new password" - - -@utils.arg('id', metavar='', help='User ID to delete') -def do_user_delete(kc, args): - """Delete user""" - kc.users.delete(args.id) - - -def do_tenant_list(kc, args): - """List all tenants""" - tenants = kc.tenants.list() - utils.print_list(tenants, ['id', 'name', 'enabled'], order_by='name') - - -@utils.arg('id', metavar='', help='Tenant ID to display') -def do_tenant_get(kc, args): - """Display tenant details""" - tenant = kc.tenants.get(args.id) - utils.print_dict(tenant._info) - - -@utils.arg('--name', metavar='', required=True, - help='New tenant name (must be unique)') -@utils.arg('--description', metavar='', default=None, - help='Description of new tenant (default is none)') -@utils.arg('--enabled', metavar='', default=True, - help='Initial tenant enabled status (default true)') -def do_tenant_create(kc, args): - """Create new tenant""" - tenant = kc.tenants.create(args.name, - description=args.description, - enabled=utils.string_to_bool(args.enabled)) - utils.print_dict(tenant._info) - - -@utils.arg('--name', metavar='', - help='Desired new name of tenant') -@utils.arg('--description', metavar='', default=None, - help='Desired new description of tenant') -@utils.arg('--enabled', metavar='', - help='Enable or disable tenant') -@utils.arg('id', metavar='', help='Tenant ID to update') -def do_tenant_update(kc, args): - """Update tenant name, description, enabled status""" - tenant = kc.tenants.get(args.id) - kwargs = {} - if args.name: - kwargs.update({'name': args.name}) - if args.description is not None: - kwargs.update({'description': args.description}) - if args.enabled: - kwargs.update({'enabled': utils.string_to_bool(args.enabled)}) - - if kwargs == {}: - print "Tenant not updated, no arguments present." - return - tenant.update(**kwargs) - - -@utils.arg('id', metavar='', help='Tenant ID to delete') -def do_tenant_delete(kc, args): - """Delete tenant""" - kc.tenants.delete(args.id) - - -@utils.arg('--name', metavar='', required=True, - help='Name of new service (must be unique)') -@utils.arg('--type', metavar='', required=True, - help='Service type (one of: identity, compute, network, ' - 'image, or object-store)') -@utils.arg('--description', metavar='', - help='Description of service') -def do_service_create(kc, args): - """Add service to Service Catalog""" - service = kc.services.create(args.name, - args.type, - args.description) - utils.print_dict(service._info) - - -def do_service_list(kc, args): - """List all services in Service Catalog""" - services = kc.services.list() - utils.print_list(services, ['id', 'name', 'type', 'description'], - order_by='name') - - -@utils.arg('id', metavar='', help='Service ID to display') -def do_service_get(kc, args): - """Display service from Service Catalog""" - service = kc.services.get(args.id) - utils.print_dict(service._info) - - -@utils.arg('id', metavar='', help='Service ID to delete') -def do_service_delete(kc, args): - """Delete service from Service Catalog""" - kc.services.delete(args.id) - - -def do_role_list(kc, args): - """List all roles""" - roles = kc.roles.list() - utils.print_list(roles, ['id', 'name'], order_by='name') - - -@utils.arg('id', metavar='', help='Role ID to display') -def do_role_get(kc, args): - """Display role details""" - role = kc.roles.get(args.id) - utils.print_dict(role._info) - - -@utils.arg('--name', metavar='', required=True, - help='Name of new role') -def do_role_create(kc, args): - """Create new role""" - role = kc.roles.create(args.name) - utils.print_dict(role._info) - - -@utils.arg('id', metavar='', help='Role ID to delete') -def do_role_delete(kc, args): - """Delete role""" - kc.roles.delete(args.id) - - -# TODO(jakedahn): refactor this to allow role, user, and tenant names. -@utils.arg('--user-id', '--user_id', metavar='', required=True, - help='User ID') -@utils.arg('--role-id', '--role_id', metavar='', required=True, - help='Role ID') -@utils.arg('--tenant-id', metavar='', help='Tenant ID') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -def do_user_role_add(kc, args): - """Add role to user""" - kc.roles.add_user_role(args.user_id, args.role_id, args.tenant_id) - - -# TODO(jakedahn): refactor this to allow role, user, and tenant names. -@utils.arg('--user-id', '--user_id', metavar='', required=True, - help='User ID') -@utils.arg('--role-id', '--role_id', metavar='', required=True, - help='Role ID') -@utils.arg('--tenant-id', metavar='', help='Tenant ID') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -def do_user_role_remove(kc, args): - """Remove role from user""" - kc.roles.remove_user_role(args.user_id, args.role_id, args.tenant_id) - - -@utils.arg('--user-id', metavar='', - help='List roles granted to a user') -@utils.arg('--user_id', help=argparse.SUPPRESS) -@utils.arg('--tenant-id', metavar='', - help='List roles granted on a tenant') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -def do_user_role_list(kc, args): - """List roles granted to a user""" - if not args.tenant_id: - # use the authenticated tenant id as a default - args.tenant_id = kc.auth_tenant_id - if not args.user_id: - # use the authenticated user id as a default - args.user_id = kc.auth_user_id - roles = kc.roles.roles_for_user(user=args.user_id, tenant=args.tenant_id) - - # this makes the command output a bit more intuitive - for role in roles: - role.user_id = args.user_id - role.tenant_id = args.tenant_id - - utils.print_list(roles, ['id', 'name', 'user_id', 'tenant_id'], - order_by='name') - - -@utils.arg('--user-id', metavar='', help='User ID') -@utils.arg('--user_id', help=argparse.SUPPRESS) -@utils.arg('--tenant-id', metavar='', help='Tenant ID') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -def do_ec2_credentials_create(kc, args): - """Create EC2-compatible credentials for user per tenant""" - if not args.tenant_id: - # use the authenticated tenant id as a default - args.tenant_id = kc.auth_tenant_id - if not args.user_id: - # use the authenticated user id as a default - args.user_id = kc.auth_user_id - credentials = kc.ec2.create(args.user_id, args.tenant_id) - utils.print_dict(credentials._info) - - -@utils.arg('--user-id', metavar='', help='User ID') -@utils.arg('--user_id', help=argparse.SUPPRESS) -@utils.arg('--access', metavar='', required=True, - help='Access Key') -def do_ec2_credentials_get(kc, args): - """Display EC2-compatible credentials""" - if not args.user_id: - # use the authenticated user id as a default - args.user_id = kc.auth_user_id - cred = kc.ec2.get(args.user_id, args.access) - if cred: - utils.print_dict(cred._info) - - -@utils.arg('--user-id', metavar='', help='User ID') -@utils.arg('--user_id', help=argparse.SUPPRESS) -def do_ec2_credentials_list(kc, args): - """List EC2-compatible credentials for a user""" - if not args.user_id: - # use the authenticated user id as a default - args.user_id = kc.auth_user_id - credentials = kc.ec2.list(args.user_id) - for cred in credentials: - try: - cred.tenant = getattr(kc.tenants.get(cred.tenant_id), 'name') - except Exception: - # FIXME(dtroyer): Retrieving the tenant name fails for normal - # users; stuff in the tenant_id instead. - cred.tenant = cred.tenant_id - utils.print_list(credentials, ['tenant', 'access', 'secret']) - - -@utils.arg('--user-id', metavar='', help='User ID') -@utils.arg('--user_id', help=argparse.SUPPRESS) -@utils.arg('--access', metavar='', required=True, - help='Access Key') -def do_ec2_credentials_delete(kc, args): - """Delete EC2-compatible credentials""" - if not args.user_id: - # use the authenticated user id as a default - args.user_id = kc.auth_user_id - try: - kc.ec2.delete(args.user_id, args.access) - print 'Credential has been deleted.' - except Exception as e: - print 'Unable to delete credential: %s' % e - - -@utils.arg('--service', metavar='', default=None, - help='Service type to return') -@require_service_catalog -def do_catalog(kc, args): - """List service catalog, possibly filtered by service.""" - endpoints = kc.service_catalog.get_endpoints(service_type=args.service) - for (service, service_endpoints) in endpoints.iteritems(): - if len(service_endpoints) > 0: - print "Service: %s" % service - for ep in service_endpoints: - utils.print_dict(ep) - - -@utils.arg('--service', metavar='', required=True, - help='Service type to select') -@utils.arg('--endpoint-type', metavar='', default='publicURL', - help='Endpoint type to select') -@utils.arg('--endpoint_type', default='publicURL', - help=argparse.SUPPRESS) -@utils.arg('--attr', metavar='', - help='Service attribute to match for selection') -@utils.arg('--value', metavar='', - help='Value of attribute to match') -@require_service_catalog -def do_endpoint_get(kc, args): - """Find endpoint filtered by a specific attribute or service type""" - kwargs = { - 'service_type': args.service, - 'endpoint_type': args.endpoint_type, - } - - if args.attr and args.value: - kwargs.update({'attr': args.attr, 'filter_value': args.value}) - elif args.attr or args.value: - print 'Both --attr and --value required.' - return - - url = kc.service_catalog.url_for(**kwargs) - utils.print_dict({'%s.%s' % (args.service, args.endpoint_type): url}) - - -def do_endpoint_list(kc, args): - """List configured service endpoints""" - endpoints = kc.endpoints.list() - utils.print_list(endpoints, - ['id', 'region', 'publicurl', - 'internalurl', 'adminurl', 'service_id']) - - -@utils.arg('--region', metavar='', - help='Endpoint region', default='regionOne') -@utils.arg('--service-id', '--service_id', metavar='', - required=True, help='ID of service associated with Endpoint') -@utils.arg('--publicurl', metavar='', - help='Public URL endpoint') -@utils.arg('--adminurl', metavar='', - help='Admin URL endpoint') -@utils.arg('--internalurl', metavar='', - help='Internal URL endpoint') -def do_endpoint_create(kc, args): - """Create a new endpoint associated with a service""" - endpoint = kc.endpoints.create(args.region, - args.service_id, - args.publicurl, - args.adminurl, - args.internalurl) - utils.print_dict(endpoint._info) - - -@utils.arg('id', metavar='', help='ID of endpoint to delete') -def do_endpoint_delete(kc, args): - """Delete a service endpoint""" - try: - kc.endpoints.delete(args.id) - print 'Endpoint has been deleted.' - except Exception: - print 'Unable to delete endpoint.' - - -@utils.arg('--wrap', metavar='', default=0, - help='wrap PKI tokens to a specified length, or 0 to disable') -@require_service_catalog -def do_token_get(kc, args): - """Display the current user token""" - utils.print_dict(kc.service_catalog.get_token(), - wrap=int(args.wrap)) diff --git a/keystoneclient/v2_0/tenants.py b/keystoneclient/v2_0/tenants.py index e15341d31..91731c45e 100644 --- a/keystoneclient/v2_0/tenants.py +++ b/keystoneclient/v2_0/tenants.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -14,13 +14,15 @@ # License for the specific language governing permissions and limitations # under the License. -import urllib +from keystoneauth1 import plugin +import urllib.parse from keystoneclient import base +from keystoneclient import exceptions class Tenant(base.Resource): - """Represents a Keystone tenant + """Represents a Keystone tenant. Attributes: * id: a uuid that identifies the tenant @@ -29,7 +31,9 @@ class Tenant(base.Resource): * enabled: boolean to indicate if tenant is enabled """ + def __repr__(self): + """Return string representation of tenant resource information.""" return "" % self._info def delete(self): @@ -54,40 +58,47 @@ def update(self, name=None, description=None, enabled=None): return retval def add_user(self, user, role): - return self.manager.api.roles.add_user_role(base.getid(user), - base.getid(role), - self.id) - - def remove_user(self, user, role): - return self.manager.api.roles.remove_user_role(base.getid(user), + return self.manager.role_manager.add_user_role(base.getid(user), base.getid(role), self.id) + def remove_user(self, user, role): + return self.manager.role_manager.remove_user_role(base.getid(user), + base.getid(role), + self.id) + def list_users(self): return self.manager.list_users(self.id) class TenantManager(base.ManagerWithFind): - """Manager class for manipulating Keystone tenants""" + """Manager class for manipulating Keystone tenants.""" + resource_class = Tenant + def __init__(self, client, role_manager, user_manager): + super(TenantManager, self).__init__(client) + self.role_manager = role_manager + self.user_manager = user_manager + def get(self, tenant_id): return self._get("/tenants/%s" % tenant_id, "tenant") - def create(self, tenant_name, description=None, enabled=True): - """ - Create a new tenant. - - """ + def create(self, tenant_name, description=None, enabled=True, **kwargs): + """Create a new tenant.""" params = {"tenant": {"name": tenant_name, "description": description, "enabled": enabled}} - return self._create('/tenants', params, "tenant") + # Allow Extras Passthru and ensure we don't clobber primary arguments. + for k, v in kwargs.items(): + if k not in params['tenant']: + params['tenant'][k] = v + + return self._post('/tenants', params, "tenant") def list(self, limit=None, marker=None): - """ - Get a list of tenants. + """Get a list of tenants. :param integer limit: maximum number to return. (optional) :param string marker: use when specifying a limit and making @@ -96,7 +107,6 @@ def list(self, limit=None, marker=None): :rtype: list of :class:`Tenant` """ - params = {} if limit: params['limit'] = limit @@ -105,24 +115,23 @@ def list(self, limit=None, marker=None): query = "" if params: - query = "?" + urllib.urlencode(params) - - reset = 0 - if self.api.management_url is None: - # special casing to allow tenant lists on the auth_url - # for unscoped tokens - reset = 1 - self.api.management_url = self.api.auth_url - tenant_list = self._list("/tenants%s" % query, "tenants") - if reset: - self.api.management_url = None + query = "?" + urllib.parse.urlencode(params) + + # NOTE(jamielennox): try doing a regular admin query first. If there is + # no endpoint that can satisfy the request (eg an unscoped token) then + # issue it against the auth_url. + try: + tenant_list = self._list('/tenants%s' % query, 'tenants') + except exceptions.EndpointNotFound: + endpoint_filter = {'interface': plugin.AUTH_INTERFACE} + tenant_list = self._list('/tenants%s' % query, 'tenants', + endpoint_filter=endpoint_filter) + return tenant_list def update(self, tenant_id, tenant_name=None, description=None, - enabled=None): - """ - Update a tenant with a new name and description. - """ + enabled=None, **kwargs): + """Update a tenant with a new name and description.""" body = {"tenant": {'id': tenant_id}} if tenant_name is not None: body['tenant']['name'] = tenant_name @@ -130,27 +139,31 @@ def update(self, tenant_id, tenant_name=None, description=None, body['tenant']['enabled'] = enabled if description is not None: body['tenant']['description'] = description + + # Allow Extras Passthru and ensure we don't clobber primary arguments. + for k, v in kwargs.items(): + if k not in body['tenant']: + body['tenant'][k] = v + # Keystone's API uses a POST rather than a PUT here. - return self._create("/tenants/%s" % tenant_id, body, "tenant") + return self._post("/tenants/%s" % tenant_id, body, "tenant") def delete(self, tenant): - """ - Delete a tenant. - """ + """Delete a tenant.""" return self._delete("/tenants/%s" % (base.getid(tenant))) def list_users(self, tenant): - """ List users for a tenant. """ - return self.api.users.list(base.getid(tenant)) + """List users for a tenant.""" + return self.user_manager.list(base.getid(tenant)) def add_user(self, tenant, user, role): - """ Add a user to a tenant with the given role. """ - return self.api.roles.add_user_role(base.getid(user), - base.getid(role), - base.getid(tenant)) - - def remove_user(self, tenant, user, role): - """ Remove the specified role from the user on the tenant. """ - return self.api.roles.remove_user_role(base.getid(user), + """Add a user to a tenant with the given role.""" + return self.role_manager.add_user_role(base.getid(user), base.getid(role), base.getid(tenant)) + + def remove_user(self, tenant, user, role): + """Remove the specified role from the user on the tenant.""" + return self.role_manager.remove_user_role(base.getid(user), + base.getid(role), + base.getid(tenant)) diff --git a/keystoneclient/v2_0/tokens.py b/keystoneclient/v2_0/tokens.py index c129db771..14c054eeb 100644 --- a/keystoneclient/v2_0/tokens.py +++ b/keystoneclient/v2_0/tokens.py @@ -1,8 +1,26 @@ +# 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 plugin + +from keystoneclient import access from keystoneclient import base +from keystoneclient.i18n import _ class Token(base.Resource): def __repr__(self): + """Return string representation of resource information.""" return "" % self._info @property @@ -15,10 +33,10 @@ def expires(self): @property def tenant(self): - return self._info['token'].get('tenant', None) + return self._info['token'].get('tenant') -class TokenManager(base.ManagerWithFind): +class TokenManager(base.Manager): resource_class = Token def authenticate(self, username=None, tenant_id=None, tenant_name=None, @@ -29,19 +47,25 @@ def authenticate(self, username=None, tenant_id=None, tenant_name=None, params = {"auth": {"passwordCredentials": {"username": username, "password": password}}} else: - raise ValueError('A username and password or token is required.') + raise ValueError( + _('A username and password or token is required.')) if tenant_id: params['auth']['tenantId'] = tenant_id elif tenant_name: params['auth']['tenantName'] = tenant_name - reset = 0 - if self.api.management_url is None: - reset = 1 - self.api.management_url = self.api.auth_url - token_ref = self._create('/tokens', params, "access", - return_raw=return_raw) - if reset: - self.api.management_url = None + + args = ['/tokens', params, 'access'] + kwargs = {'return_raw': return_raw, 'log': False} + + # NOTE(jamielennox): try doing a regular admin query first. If there is + # no endpoint that can satisfy the request (eg an unscoped token) then + # issue it against the auth_url. + try: + token_ref = self._post(*args, **kwargs) + except exceptions.EndpointNotFound: + kwargs['endpoint_filter'] = {'interface': plugin.AUTH_INTERFACE} + token_ref = self._post(*args, **kwargs) + return token_ref def delete(self, token): @@ -49,3 +73,53 @@ def delete(self, token): def endpoints(self, token): return self._get("/tokens/%s/endpoints" % base.getid(token), "token") + + def validate(self, token): + """Validate a token. + + :param token: Token to be validated. + + :rtype: :py:class:`.Token` + + """ + return self._get('/tokens/%s' % base.getid(token), 'access') + + def get_token_data(self, token): + """Fetch the data about a token from the identity server. + + :param str token: The token id. + + :rtype: dict + """ + url = '/tokens/%s' % token + resp, body = self.client.get(url) + return body + + def validate_access_info(self, token): + """Validate a token. + + :param token: Token to be validated. This can be an instance of + :py:class:`keystoneclient.access.AccessInfo` or a string + token_id. + + :rtype: :py:class:`keystoneclient.access.AccessInfoV2` + + """ + def calc_id(token): + if isinstance(token, access.AccessInfo): + return token.auth_token + return base.getid(token) + + token_id = calc_id(token) + body = self.get_token_data(token_id) + return access.AccessInfo.factory(auth_token=token_id, body=body) + + def get_revoked(self): + """Return the revoked tokens response. + + The response will be a dict containing 'signed' which is a CMS-encoded + document. + + """ + resp, body = self.client.get('/tokens/revoked') + return body diff --git a/keystoneclient/v2_0/users.py b/keystoneclient/v2_0/users.py index 97dc72f73..706dafcdc 100644 --- a/keystoneclient/v2_0/users.py +++ b/keystoneclient/v2_0/users.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -14,14 +14,15 @@ # License for the specific language governing permissions and limitations # under the License. -import urllib - from keystoneclient import base +import urllib.parse class User(base.Resource): - """Represents a Keystone user""" + """Represents a Keystone user.""" + def __repr__(self): + """Return string representation of user resource information.""" return "" % self._info def delete(self): @@ -32,103 +33,91 @@ def list_roles(self, tenant=None): class UserManager(base.ManagerWithFind): - """Manager class for manipulating Keystone users""" + """Manager class for manipulating Keystone users.""" + resource_class = User + def __init__(self, client, role_manager): + super(UserManager, self).__init__(client) + self.role_manager = role_manager + def get(self, user): return self._get("/users/%s" % base.getid(user), "user") def update(self, user, **kwargs): - """ - Update user data. + """Update user data. Supported arguments include ``name``, ``email``, and ``enabled``. """ # FIXME(gabriel): "tenantId" seems to be accepted by the API but # fails to actually update the default tenant. params = {"user": kwargs} - params['user']['id'] = base.getid(user) url = "/users/%s" % base.getid(user) return self._update(url, params, "user") def update_enabled(self, user, enabled): - """ - Update enabled-ness - """ - params = {"user": {"id": base.getid(user), - "enabled": enabled}} + """Update enabled-ness.""" + params = {"user": {"enabled": enabled}} self._update("/users/%s/OS-KSADM/enabled" % base.getid(user), params, "user") def update_password(self, user, password): - """ - Update password - """ - params = {"user": {"id": base.getid(user), - "password": password}} + """Update password.""" + params = {"user": {"password": password}} return self._update("/users/%s/OS-KSADM/password" % base.getid(user), - params, "user") + params, "user", log=False) def update_own_password(self, origpasswd, passwd): - """ - Update password - """ + """Update password.""" params = {"user": {"password": passwd, "original_password": origpasswd}} - return self._update("/OS-KSCRUD/users/%s" % self.api.user_id, params, + return self._update("/OS-KSCRUD/users/%s" % self.client.user_id, + params, response_key="access", method="PATCH", - management=False) + endpoint_filter={'interface': 'public'}, + log=False) def update_tenant(self, user, tenant): - """ - Update default tenant. - """ - params = {"user": {"id": base.getid(user), - "tenantId": base.getid(tenant)}} + """Update default tenant.""" + params = {"user": {"tenantId": base.getid(tenant)}} # FIXME(ja): seems like a bad url - default tenant is an attribute # not a subresource!??? return self._update("/users/%s/OS-KSADM/tenant" % base.getid(user), params, "user") - def create(self, name, password, email, tenant_id=None, enabled=True): - """ - Create a user. - """ - # FIXME(ja): email should be optional, keystone currently requires it + def create(self, name, password=None, email=None, + tenant_id=None, enabled=True): + """Create a user.""" params = {"user": {"name": name, "password": password, "tenantId": tenant_id, "email": email, "enabled": enabled}} - return self._create('/users', params, "user") + return self._post('/users', params, "user", log=not bool(password)) def delete(self, user): - """ - Delete a user. - """ + """Delete a user.""" return self._delete("/users/%s" % base.getid(user)) def list(self, tenant_id=None, limit=None, marker=None): - """ - Get a list of users (optionally limited to a tenant) + """Get a list of users (optionally limited to a tenant). :rtype: list of :class:`User` """ - params = {} if limit: params['limit'] = int(limit) if marker: - params['marker'] = int(marker) + params['marker'] = marker query = "" if params: - query = "?" + urllib.urlencode(params) + query = "?" + urllib.parse.urlencode(params) if not tenant_id: return self._list("/users%s" % query, "users") @@ -137,5 +126,5 @@ def list(self, tenant_id=None, limit=None, marker=None): "users") def list_roles(self, user, tenant=None): - return self.api.roles.roles_for_user(base.getid(user), - base.getid(tenant)) + return self.role_manager.roles_for_user(base.getid(user), + base.getid(tenant)) diff --git a/keystoneclient/v3/__init__.py b/keystoneclient/v3/__init__.py index feb2536b8..fcb00e066 100644 --- a/keystoneclient/v3/__init__.py +++ b/keystoneclient/v3/__init__.py @@ -1 +1,7 @@ -from keystoneclient.v3.client import Client + +from keystoneclient.v3.client import Client # noqa + + +__all__ = ( + 'client', +) diff --git a/keystoneclient/v3/access_rules.py b/keystoneclient/v3/access_rules.py new file mode 100644 index 000000000..78fd0159e --- /dev/null +++ b/keystoneclient/v3/access_rules.py @@ -0,0 +1,118 @@ +# Copyright 2019 SUSE LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ + + +class AccessRule(base.Resource): + """Represents an Identity access rule for application credentials. + + Attributes: + * id: a uuid that identifies the access rule + * method: The request method that the application credential is + permitted to use for a given API endpoint + * path: The API path that the application credential is permitted to + access + * service: The service type identifier for the service that the + application credential is permitted to access + + """ + + pass + + +class AccessRuleManager(base.CrudManager): + """Manager class for manipulating Identity access rules.""" + + resource_class = AccessRule + collection_key = 'access_rules' + key = 'access_rule' + + def get(self, access_rule, user=None): + """Retrieve an access rule. + + :param access_rule: the access rule to be retrieved from the + server + :type access_rule: str or + :class:`keystoneclient.v3.access_rules.AccessRule` + :param string user: User ID + + :returns: the specified access rule + :rtype: + :class:`keystoneclient.v3.access_rules.AccessRule` + + """ + user = user or self.client.user_id + self.base_url = '/users/%(user)s' % {'user': user} + + return super(AccessRuleManager, self).get( + access_rule_id=base.getid(access_rule)) + + def list(self, user=None, **kwargs): + """List access rules. + + :param string user: User ID + + :returns: a list of access rules + :rtype: list of + :class:`keystoneclient.v3.access_rules.AccessRule` + """ + user = user or self.client.user_id + self.base_url = '/users/%(user)s' % {'user': user} + + return super(AccessRuleManager, self).list(**kwargs) + + def find(self, user=None, **kwargs): + """Find an access rule with attributes matching ``**kwargs``. + + :param string user: User ID + + :returns: a list of matching access rules + :rtype: list of + :class:`keystoneclient.v3.access_rules.AccessRule` + """ + user = user or self.client.user_id + self.base_url = '/users/%(user)s' % {'user': user} + + return super(AccessRuleManager, self).find(**kwargs) + + def delete(self, access_rule, user=None): + """Delete an access rule. + + :param access_rule: the access rule to be deleted + :type access_rule: str or + :class:`keystoneclient.v3.access_rules.AccessRule` + :param string user: User ID + + :returns: response object with 204 status + :rtype: :class:`requests.models.Response` + + """ + user = user or self.client.user_id + self.base_url = '/users/%(user)s' % {'user': user} + + return super(AccessRuleManager, self).delete( + access_rule_id=base.getid(access_rule)) + + def update(self): + raise exceptions.MethodNotImplemented( + _('Access rules are immutable, updating is not' + ' supported.')) + + def create(self): + raise exceptions.MethodNotImplemented( + _('Access rules can only be created as attributes of application ' + 'credentials.')) diff --git a/keystoneclient/v3/application_credentials.py b/keystoneclient/v3/application_credentials.py new file mode 100644 index 000000000..3e9286279 --- /dev/null +++ b/keystoneclient/v3/application_credentials.py @@ -0,0 +1,173 @@ +# Copyright 2018 SUSE Linux GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ +from keystoneclient import utils + + +class ApplicationCredential(base.Resource): + """Represents an Identity application credential. + + Attributes: + * id: a uuid that identifies the application credential + * user: the user who owns the application credential + * name: application credential name + * secret: application credential secret + * description: application credential description + * expires_at: expiry time + * roles: role assignments on the project + * unrestricted: whether the application credential has restrictions + applied + * access_rules: a list of access rules defining what API requests the + application credential may be used for + + """ + + pass + + +class ApplicationCredentialManager(base.CrudManager): + """Manager class for manipulating Identity application credentials.""" + + resource_class = ApplicationCredential + collection_key = 'application_credentials' + key = 'application_credential' + + def create(self, name, user=None, secret=None, description=None, + expires_at=None, roles=None, + unrestricted=False, access_rules=None, **kwargs): + """Create a credential. + + :param string name: application credential name + :param string user: User ID + :param secret: application credential secret + :param description: application credential description + :param datetime.datetime expires_at: expiry time + :param List roles: list of roles on the project. Maybe a list of IDs + or a list of dicts specifying role name and domain + :param bool unrestricted: whether the application credential has + restrictions applied + :param List access_rules: a list of dicts representing access rules + + :returns: the created application credential + :rtype: + :class:`keystoneclient.v3.application_credentials.ApplicationCredential` + + """ + user = user or self.client.user_id + self.base_url = '/users/%(user)s' % {'user': user} + + # Convert roles list into list-of-dict API format + role_list = [] + if roles: + if not isinstance(roles, list): + roles = [roles] + for role in roles: + if isinstance(role, str): + role_list.extend([{'id': role}]) + elif isinstance(role, dict): + role_list.extend([role]) + else: + msg = (_("Roles must be a list of IDs or role dicts.")) + raise exceptions.CommandError(msg) + + if not role_list: + role_list = None + + # Convert datetime.datetime expires_at to iso format string + if expires_at: + expires_str = utils.isotime(at=expires_at, subsecond=True) + else: + expires_str = None + + return super(ApplicationCredentialManager, self).create( + name=name, + secret=secret, + description=description, + expires_at=expires_str, + roles=role_list, + unrestricted=unrestricted, + access_rules=access_rules, + **kwargs) + + def get(self, application_credential, user=None): + """Retrieve an application credential. + + :param application_credential: the credential to be retrieved from the + server + :type applicationcredential: str or + :class:`keystoneclient.v3.application_credentials.ApplicationCredential` + + :returns: the specified application credential + :rtype: + :class:`keystoneclient.v3.application_credentials.ApplicationCredential` + + """ + user = user or self.client.user_id + self.base_url = '/users/%(user)s' % {'user': user} + + return super(ApplicationCredentialManager, self).get( + application_credential_id=base.getid(application_credential)) + + def list(self, user=None, **kwargs): + """List application credentials. + + :param string user: User ID + + :returns: a list of application credentials + :rtype: list of + :class:`keystoneclient.v3.application_credentials.ApplicationCredential` + """ + user = user or self.client.user_id + self.base_url = '/users/%(user)s' % {'user': user} + + return super(ApplicationCredentialManager, self).list(**kwargs) + + def find(self, user=None, **kwargs): + """Find an application credential with attributes matching ``**kwargs``. # noqa + + :param string user: User ID + + :returns: a list of matching application credentials + :rtype: list of + :class:`keystoneclient.v3.application_credentials.ApplicationCredential` + """ + user = user or self.client.user_id + self.base_url = '/users/%(user)s' % {'user': user} + + return super(ApplicationCredentialManager, self).find(**kwargs) + + def delete(self, application_credential, user=None): + """Delete an application credential. + + :param application_credential: the application credential to be deleted + :type credential: str or + :class:`keystoneclient.v3.application_credentials.ApplicationCredential` + + :returns: response object with 204 status + :rtype: :class:`requests.models.Response` + + """ + user = user or self.client.user_id + self.base_url = '/users/%(user)s' % {'user': user} + + return super(ApplicationCredentialManager, self).delete( + application_credential_id=base.getid(application_credential)) + + def update(self): + raise exceptions.MethodNotImplemented( + _('Application credentials are immutable, updating is not' + ' supported.')) diff --git a/keystoneclient/v3/auth.py b/keystoneclient/v3/auth.py new file mode 100644 index 000000000..4d85e24ea --- /dev/null +++ b/keystoneclient/v3/auth.py @@ -0,0 +1,92 @@ +# 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 plugin + +from keystoneclient import base +from keystoneclient.v3 import domains +from keystoneclient.v3 import projects +from keystoneclient.v3 import system + + +Domain = domains.Domain +Project = projects.Project +System = system.System + + +class AuthManager(base.Manager): + """Retrieve auth context specific information. + + The information returned by the auth routes is entirely dependent on the + authentication information provided by the user. + """ + + _PROJECTS_URL = '/auth/projects' + _DOMAINS_URL = '/auth/domains' + _SYSTEM_URL = '/auth/system' + + def projects(self): + """List projects that the specified token can be rescoped to. + + :returns: a list of projects. + :rtype: list of :class:`keystoneclient.v3.projects.Project` + + """ + try: + return self._list(self._PROJECTS_URL, + 'projects', + obj_class=Project) + except exceptions.EndpointNotFound: + endpoint_filter = {'interface': plugin.AUTH_INTERFACE} + return self._list(self._PROJECTS_URL, + 'projects', + obj_class=Project, + endpoint_filter=endpoint_filter) + + def domains(self): + """List Domains that the specified token can be rescoped to. + + :returns: a list of domains. + :rtype: list of :class:`keystoneclient.v3.domains.Domain`. + + """ + try: + return self._list(self._DOMAINS_URL, + 'domains', + obj_class=Domain) + except exceptions.EndpointNotFound: + endpoint_filter = {'interface': plugin.AUTH_INTERFACE} + return self._list(self._DOMAINS_URL, + 'domains', + obj_class=Domain, + endpoint_filter=endpoint_filter) + + def systems(self): + """List Systems that the specified token can be rescoped to. + + At the moment this is either empty or "all". + + :returns: a list of systems. + :rtype: list of :class:`keystoneclient.v3.systems.System`. + + """ + try: + return self._list(self._SYSTEM_URL, + 'system', + obj_class=System) + except exceptions.EndpointNotFound: + endpoint_filter = {'interface': plugin.AUTH_INTERFACE} + return self._list(self._SYSTEM_URL, + 'system', + obj_class=System, + endpoint_filter=endpoint_filter) diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index 42b34a745..e99b06549 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -12,76 +12,342 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -import json + import logging +import warnings + +from oslo_serialization import jsonutils -from keystoneclient.v2_0 import client +from keystoneclient.auth.identity import v3 as v3_auth +from keystoneclient import exceptions +from keystoneclient import httpclient +from keystoneclient.i18n import _ +from keystoneclient.v3 import access_rules +from keystoneclient.v3 import application_credentials +from keystoneclient.v3 import auth +from keystoneclient.v3.contrib import endpoint_filter +from keystoneclient.v3.contrib import endpoint_policy +from keystoneclient.v3.contrib import federation +from keystoneclient.v3.contrib import oauth1 +from keystoneclient.v3.contrib import simple_cert +from keystoneclient.v3.contrib import trusts from keystoneclient.v3 import credentials -from keystoneclient.v3 import endpoints +from keystoneclient.v3 import domain_configs from keystoneclient.v3 import domains +from keystoneclient.v3 import ec2 +from keystoneclient.v3 import endpoint_groups +from keystoneclient.v3 import endpoints from keystoneclient.v3 import groups +from keystoneclient.v3 import limits from keystoneclient.v3 import policies from keystoneclient.v3 import projects +from keystoneclient.v3 import regions +from keystoneclient.v3 import registered_limits +from keystoneclient.v3 import role_assignments from keystoneclient.v3 import roles from keystoneclient.v3 import services +from keystoneclient.v3 import tokens from keystoneclient.v3 import users _logger = logging.getLogger(__name__) -class Client(client.Client): - """Client for the OpenStack Identity API v3. +class Client(httpclient.HTTPClient): + r"""Client for the OpenStack Identity API v3. + :param session: Session for requests. (optional) + :type session: keystoneauth1.session.Session + :param string user_id: User ID for authentication. (optional) :param string username: Username for authentication. (optional) + :param string user_domain_id: User's domain ID for authentication. + (optional) + :param string user_domain_name: User's domain name for authentication. + (optional) :param string password: Password for authentication. (optional) :param string token: Token for authentication. (optional) - :param string tenant_name: Tenant id. (optional) - :param string tenant_id: Tenant name. (optional) - :param string auth_url: Keystone service endpoint for authorization. + :param string domain_id: Domain ID for domain scoping. (optional) + :param string domain_name: Domain name for domain scoping. (optional) + :param string project_id: Project ID for project scoping. (optional) + :param string project_name: Project name for project scoping. (optional) + :param string project_domain_id: Project's domain ID for project + scoping. (optional) + :param string project_domain_name: Project's domain name for project + scoping. (optional) + :param string tenant_name: Tenant name. (optional) + The tenant_name keyword argument is deprecated + as of the 1.7.0 release in favor of project_name + and may be removed in the 2.0.0 release. + :param string tenant_id: Tenant id. (optional) + The tenant_id keyword argument is deprecated as of + the 1.7.0 release in favor of project_id and may + be removed in the 2.0.0 release. + :param string auth_url: Identity service endpoint for authorization. :param string region_name: Name of a region to select when choosing an endpoint from the service catalog. - :param string endpoint: A user-supplied endpoint URL for the keystone + :param string endpoint: A user-supplied endpoint URL for the identity service. Lazy-authentication is possible for API service calls if endpoint is set at - instantiation.(optional) + instantiation. (optional) :param integer timeout: Allows customization of the timeout for client http requests. (optional) + .. warning:: + + Constructing an instance of this class without a session is + deprecated as of the 1.7.0 release and will be removed in the + 2.0.0 release. + Example:: + >>> from keystoneauth1.identity import v3 + >>> from keystoneauth1 import session >>> from keystoneclient.v3 import client - >>> keystone = client.Client(username=USER, - password=PASS, - tenant_name=TENANT_NAME, - auth_url=KEYSTONE_URL) - >>> keystone.tenants.list() + >>> auth = v3.Password(user_domain_name=DOMAIN_NAME, + ... username=USER, + ... password=PASS, + ... project_domain_name=PROJECT_DOMAIN_NAME, + ... project_name=PROJECT_NAME, + ... auth_url=KEYSTONE_URL) + >>> sess = session.Session(auth=auth) + >>> keystone = client.Client(session=sess) + >>> keystone.projects.list() ... >>> user = keystone.users.get(USER_ID) >>> user.delete() + Instances of this class have the following managers: + + .. py:attribute:: credentials + + :py:class:`keystoneclient.v3.credentials.CredentialManager` + + .. py:attribute:: domain_configs + + :py:class:`keystoneclient.v3.domain_configs.DomainConfigManager` + + .. py:attribute:: ec2 + + :py:class:`keystoneclient.v3.ec2.EC2Manager` + + .. py:attribute:: endpoint_filter + + :py:class:`keystoneclient.v3.contrib.endpoint_filter.\ + EndpointFilterManager` + + .. py:attribute:: endpoint_groups + + :py:class:`keystoneclient.v3.endpoint_groups.\ + EndpointGroupManager` + + .. py:attribute:: endpoint_policy + + :py:class:`keystoneclient.v3.contrib.endpoint_policy.\ + EndpointPolicyManager` + + .. py:attribute:: endpoints + + :py:class:`keystoneclient.v3.endpoints.EndpointManager` + + .. py:attribute:: domains + + :py:class:`keystoneclient.v3.domains.DomainManager` + + .. py:attribute:: federation + + :py:class:`keystoneclient.v3.contrib.federation.core.FederationManager` + + .. py:attribute:: groups + + :py:class:`keystoneclient.v3.groups.GroupManager` + + .. py:attribute:: limits + + :py:class:`keystoneclient.v3.limits.LimitManager` + + .. py:attribute:: oauth1 + + :py:class:`keystoneclient.v3.contrib.oauth1.core.OAuthManager` + + .. py:attribute:: policies + + :py:class:`keystoneclient.v3.policies.PolicyManager` + + .. py:attribute:: regions + + :py:class:`keystoneclient.v3.regions.RegionManager` + + .. py:attribute:: registered_limits + + :py:class:`keystoneclient.v3.registered_limits.RegisteredLimitManager` + + .. py:attribute:: role_assignments + + :py:class:`keystoneclient.v3.role_assignments.RoleAssignmentManager` + + .. py:attribute:: roles + + :py:class:`keystoneclient.v3.roles.RoleManager` + + .. py:attribute:: simple_cert + + :py:class:`keystoneclient.v3.contrib.simple_cert.SimpleCertManager` + + .. py:attribute:: services + + :py:class:`keystoneclient.v3.services.ServiceManager` + + .. py:attribute:: tokens + + :py:class:`keystoneclient.v3.tokens.TokenManager` + + .. py:attribute:: trusts + + :py:class:`keystoneclient.v3.contrib.trusts.TrustManager` + + .. py:attribute:: users + + :py:class:`keystoneclient.v3.users.UserManager` + """ - def __init__(self, endpoint=None, **kwargs): - """ Initialize a new client for the Keystone v2.0 API. """ - super(Client, self).__init__(endpoint=endpoint, **kwargs) - - self.credentials = credentials.CredentialManager(self) - self.endpoints = endpoints.EndpointManager(self) - self.domains = domains.DomainManager(self) - self.groups = groups.GroupManager(self) - self.policies = policies.PolicyManager(self) - self.projects = projects.ProjectManager(self) - self.roles = roles.RoleManager(self) - self.services = services.ServiceManager(self) - self.users = users.UserManager(self) - - # NOTE(gabriel): If we have a pre-defined endpoint then we can - # get away with lazy auth. Otherwise auth immediately. - if endpoint: - self.management_url = endpoint - else: + version = 'v3' + + def __init__(self, **kwargs): + """Initialize a new client for the Keystone v3 API.""" + super(Client, self).__init__(**kwargs) + + if not kwargs.get('session'): + warnings.warn( + 'Constructing an instance of the ' + 'keystoneclient.v3.client.Client class without a session is ' + 'deprecated as of the 1.7.0 release and may be removed in ' + 'the 2.0.0 release.', DeprecationWarning) + + self.access_rules = ( + access_rules.AccessRuleManager(self._adapter) + ) + self.application_credentials = ( + application_credentials.ApplicationCredentialManager(self._adapter) + ) + self.auth = auth.AuthManager(self._adapter) + self.credentials = credentials.CredentialManager(self._adapter) + self.ec2 = ec2.EC2Manager(self._adapter) + self.endpoint_filter = endpoint_filter.EndpointFilterManager( + self._adapter) + self.endpoint_groups = endpoint_groups.EndpointGroupManager( + self._adapter) + self.endpoint_policy = endpoint_policy.EndpointPolicyManager( + self._adapter) + self.endpoints = endpoints.EndpointManager(self._adapter) + self.domain_configs = domain_configs.DomainConfigManager(self._adapter) + self.domains = domains.DomainManager(self._adapter) + self.federation = federation.FederationManager(self._adapter) + self.groups = groups.GroupManager(self._adapter) + self.limits = limits.LimitManager(self._adapter) + self.oauth1 = oauth1.create_oauth_manager(self._adapter) + self.policies = policies.PolicyManager(self._adapter) + self.projects = projects.ProjectManager(self._adapter) + self.registered_limits = registered_limits.RegisteredLimitManager( + self._adapter) + self.regions = regions.RegionManager(self._adapter) + self.role_assignments = ( + role_assignments.RoleAssignmentManager(self._adapter)) + self.roles = roles.RoleManager(self._adapter) + self.inference_rules = roles.InferenceRuleManager(self._adapter) + self.services = services.ServiceManager(self._adapter) + self.simple_cert = simple_cert.SimpleCertManager(self._adapter) + self.tokens = tokens.TokenManager(self._adapter) + self.trusts = trusts.TrustManager(self._adapter) + self.users = users.UserManager(self._adapter) + + # DEPRECATED: if session is passed then we go to the new behaviour of + # authenticating on the first required call. + if 'session' not in kwargs and self.management_url is None: self.authenticate() def serialize(self, entity): - return json.dumps(entity, sort_keys=True) + return jsonutils.dumps(entity, sort_keys=True) + + def process_token(self, **kwargs): + """Extract and process information from the new auth_ref. + + And set the relevant authentication information. + """ + super(Client, self).process_token(**kwargs) + if self.auth_ref.domain_scoped: + if not self.auth_ref.domain_id: + raise exceptions.AuthorizationFailure( + _("Token didn't provide domain_id")) + self._process_management_url(kwargs.get('region_name')) + self.domain_name = self.auth_ref.domain_name + self.domain_id = self.auth_ref.domain_id + if self._management_url: + self._management_url = self._management_url.replace('/v2.0', '/v3') + + def get_raw_token_from_identity_service(self, auth_url, user_id=None, + username=None, + user_domain_id=None, + user_domain_name=None, + password=None, + domain_id=None, domain_name=None, + project_id=None, project_name=None, + project_domain_id=None, + project_domain_name=None, + token=None, + trust_id=None, + **kwargs): + """Authenticate against the v3 Identity API. + + If password and token methods are both provided then both methods will + be used in the request. + + :returns: access.AccessInfo if authentication was successful. + :rtype: :class:`keystoneclient.access.AccessInfoV3` + :raises keystoneclient.exceptions.AuthorizationFailure: if unable to + authenticate or validate the existing authorization token. + :raises keystoneclient.exceptions.Unauthorized: if authentication fails + due to invalid token. + + """ + try: + if auth_url is None: + raise ValueError(_("Cannot authenticate without an auth_url")) + + auth_methods = [] + + if token: + auth_methods.append(v3_auth.TokenMethod(token=token)) + + if password: + m = v3_auth.PasswordMethod(user_id=user_id, + username=username, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + password=password) + auth_methods.append(m) + + if not auth_methods: + msg = _('A user and password or token is required.') + raise exceptions.AuthorizationFailure(msg) + + plugin = v3_auth.Auth(auth_url, auth_methods, + trust_id=trust_id, + domain_id=domain_id, + domain_name=domain_name, + project_id=project_id, + project_name=project_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name) + + return plugin.get_auth_ref(self.session) + except (exceptions.AuthorizationFailure, exceptions.Unauthorized): + _logger.debug('Authorization failed.') + raise + except exceptions.EndpointNotFound: + msg = _('There was no suitable authentication url for this' + ' request') + raise exceptions.AuthorizationFailure(msg) + except Exception as e: + raise exceptions.AuthorizationFailure( + _('Authorization failed: %s') % e) diff --git a/keystoneclient/v3/contrib/__init__.py b/keystoneclient/v3/contrib/__init__.py new file mode 100644 index 000000000..576bd6218 --- /dev/null +++ b/keystoneclient/v3/contrib/__init__.py @@ -0,0 +1,2 @@ + +__all__ = tuple() diff --git a/keystoneclient/v3/contrib/endpoint_filter.py b/keystoneclient/v3/contrib/endpoint_filter.py new file mode 100644 index 000000000..26d5a8745 --- /dev/null +++ b/keystoneclient/v3/contrib/endpoint_filter.py @@ -0,0 +1,163 @@ +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ +from keystoneclient.v3 import endpoint_groups +from keystoneclient.v3 import endpoints +from keystoneclient.v3 import projects + + +class EndpointFilterManager(base.Manager): + """Manager class for manipulating project-endpoint associations. + + Project-endpoint associations can be with endpoints directly or via + endpoint groups. + + """ + + OS_EP_FILTER_EXT = '/OS-EP-FILTER' + + def _build_base_url(self, project=None, endpoint=None): + project_id = base.getid(project) + endpoint_id = base.getid(endpoint) + + if project_id and endpoint_id: + api_path = '/projects/%s/endpoints/%s' % (project_id, endpoint_id) + elif project_id: + api_path = '/projects/%s/endpoints' % (project_id) + elif endpoint_id: + api_path = '/endpoints/%s/projects' % (endpoint_id) + else: + msg = _('Must specify a project, an endpoint, or both') + raise exceptions.ValidationError(msg) + + return self.OS_EP_FILTER_EXT + api_path + + def _build_group_base_url(self, project=None, endpoint_group=None): + project_id = base.getid(project) + endpoint_group_id = base.getid(endpoint_group) + + if project_id and endpoint_group_id: + api_path = '/endpoint_groups/%s/projects/%s' % ( + endpoint_group_id, project_id) + elif project_id: + api_path = '/projects/%s/endpoint_groups' % (project_id) + elif endpoint_group_id: + api_path = '/endpoint_groups/%s/projects' % (endpoint_group_id) + else: + msg = _('Must specify a project, an endpoint group, or both') + raise exceptions.ValidationError(msg) + + return self.OS_EP_FILTER_EXT + api_path + + def add_endpoint_to_project(self, project, endpoint): + """Create a project-endpoint association.""" + if not (project and endpoint): + raise ValueError(_('project and endpoint are required')) + + base_url = self._build_base_url(project=project, + endpoint=endpoint) + return super(EndpointFilterManager, self)._put(url=base_url) + + def delete_endpoint_from_project(self, project, endpoint): + """Remove a project-endpoint association.""" + if not (project and endpoint): + raise ValueError(_('project and endpoint are required')) + + base_url = self._build_base_url(project=project, + endpoint=endpoint) + return super(EndpointFilterManager, self)._delete(url=base_url) + + def check_endpoint_in_project(self, project, endpoint): + """Check if project-endpoint association exists.""" + if not (project and endpoint): + raise ValueError(_('project and endpoint are required')) + + base_url = self._build_base_url(project=project, + endpoint=endpoint) + return super(EndpointFilterManager, self)._head(url=base_url) + + def list_endpoints_for_project(self, project): + """List all endpoints for a given project.""" + if not project: + raise ValueError(_('project is required')) + + base_url = self._build_base_url(project=project) + return super(EndpointFilterManager, self)._list( + base_url, + endpoints.EndpointManager.collection_key, + obj_class=endpoints.EndpointManager.resource_class) + + def list_projects_for_endpoint(self, endpoint): + """List all projects for a given endpoint.""" + if not endpoint: + raise ValueError(_('endpoint is required')) + + base_url = self._build_base_url(endpoint=endpoint) + return super(EndpointFilterManager, self)._list( + base_url, + projects.ProjectManager.collection_key, + obj_class=projects.ProjectManager.resource_class) + + def add_endpoint_group_to_project(self, endpoint_group, project): + """Create a project-endpoint group association.""" + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_group_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._put(url=base_url) + + def delete_endpoint_group_from_project(self, endpoint_group, project): + """Remove a project-endpoint group association.""" + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_group_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._delete(url=base_url) + + def check_endpoint_group_in_project(self, endpoint_group, project): + """Check if project-endpoint group association exists.""" + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_group_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._head(url=base_url) + + def list_endpoint_groups_for_project(self, project): + """List all endpoint groups for a given project.""" + if not project: + raise ValueError(_('project is required')) + + base_url = self._build_group_base_url(project=project) + + return super(EndpointFilterManager, self)._list( + base_url, + 'endpoint_groups', + obj_class=endpoint_groups.EndpointGroupManager.resource_class) + + def list_projects_for_endpoint_group(self, endpoint_group): + """List all projects associated with a given endpoint group.""" + if not endpoint_group: + raise ValueError(_('endpoint_group is required')) + + base_url = self._build_group_base_url(endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._list( + base_url, + projects.ProjectManager.collection_key, + obj_class=projects.ProjectManager.resource_class) diff --git a/keystoneclient/v3/contrib/endpoint_policy.py b/keystoneclient/v3/contrib/endpoint_policy.py new file mode 100644 index 000000000..c65b1fbc7 --- /dev/null +++ b/keystoneclient/v3/contrib/endpoint_policy.py @@ -0,0 +1,156 @@ +# Copyright 2014 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base +from keystoneclient.i18n import _ +from keystoneclient.v3 import endpoints +from keystoneclient.v3 import policies + + +class EndpointPolicyManager(base.Manager): + """Manager class for manipulating endpoint-policy associations.""" + + OS_EP_POLICY_EXT = 'OS-ENDPOINT-POLICY' + + def _act_on_policy_association_for_endpoint( + self, policy, endpoint, action): + if not (policy and endpoint): + raise ValueError(_('policy and endpoint are required')) + + policy_id = base.getid(policy) + endpoint_id = base.getid(endpoint) + url = ('/policies/%(policy_id)s/%(ext_name)s' + '/endpoints/%(endpoint_id)s') % { + 'policy_id': policy_id, + 'ext_name': self.OS_EP_POLICY_EXT, + 'endpoint_id': endpoint_id} + return action(url=url) + + def create_policy_association_for_endpoint(self, policy, endpoint): + """Create an association between a policy and an endpoint.""" + return self._act_on_policy_association_for_endpoint( + policy, endpoint, self._put) + + def check_policy_association_for_endpoint(self, policy, endpoint): + """Check an association between a policy and an endpoint.""" + return self._act_on_policy_association_for_endpoint( + policy, endpoint, self._head) + + def delete_policy_association_for_endpoint(self, policy, endpoint): + """Delete an association between a policy and an endpoint.""" + return self._act_on_policy_association_for_endpoint( + policy, endpoint, self._delete) + + def _act_on_policy_association_for_service(self, policy, service, action): + if not (policy and service): + raise ValueError(_('policy and service are required')) + + policy_id = base.getid(policy) + service_id = base.getid(service) + url = ('/policies/%(policy_id)s/%(ext_name)s' + '/services/%(service_id)s') % { + 'policy_id': policy_id, + 'ext_name': self.OS_EP_POLICY_EXT, + 'service_id': service_id} + return action(url=url) + + def create_policy_association_for_service(self, policy, service): + """Create an association between a policy and a service.""" + return self._act_on_policy_association_for_service( + policy, service, self._put) + + def check_policy_association_for_service(self, policy, service): + """Check an association between a policy and a service.""" + return self._act_on_policy_association_for_service( + policy, service, self._head) + + def delete_policy_association_for_service(self, policy, service): + """Delete an association between a policy and a service.""" + return self._act_on_policy_association_for_service( + policy, service, self._delete) + + def _act_on_policy_association_for_region_and_service( + self, policy, region, service, action): + if not (policy and region and service): + raise ValueError(_('policy, region and service are required')) + + policy_id = base.getid(policy) + region_id = base.getid(region) + service_id = base.getid(service) + url = ('/policies/%(policy_id)s/%(ext_name)s' + '/services/%(service_id)s/regions/%(region_id)s') % { + 'policy_id': policy_id, + 'ext_name': self.OS_EP_POLICY_EXT, + 'service_id': service_id, + 'region_id': region_id} + return action(url=url) + + def create_policy_association_for_region_and_service( + self, policy, region, service): + """Create an association between a policy and a service in a region.""" + return self._act_on_policy_association_for_region_and_service( + policy, region, service, self._put) + + def check_policy_association_for_region_and_service( + self, policy, region, service): + """Check an association between a policy and a service in a region.""" + return self._act_on_policy_association_for_region_and_service( + policy, region, service, self._head) + + def delete_policy_association_for_region_and_service( + self, policy, region, service): + """Delete an association between a policy and a service in a region.""" + return self._act_on_policy_association_for_region_and_service( + policy, region, service, self._delete) + + def get_policy_for_endpoint(self, endpoint): + """Get the effective policy for an endpoint. + + :param endpoint: endpoint object or ID + + :returns: policies.Policy object + + """ + if not endpoint: + raise ValueError(_('endpoint is required')) + + endpoint_id = base.getid(endpoint) + url = ('/endpoints/%(endpoint_id)s/%(ext_name)s/policy') % { + 'endpoint_id': endpoint_id, + 'ext_name': self.OS_EP_POLICY_EXT} + + resp, body = self.client.get(url) + return self._prepare_return_value( + resp, policies.Policy(self, body[policies.PolicyManager.key], + loaded=True)) + + def list_endpoints_for_policy(self, policy): + """List endpoints with the effective association to a policy. + + :param policy: policy object or ID + + :returns: list of endpoints that are associated with the policy + + """ + if not policy: + raise ValueError(_('policy is required')) + + policy_id = base.getid(policy) + url = ('/policies/%(policy_id)s/%(ext_name)s/endpoints') % { + 'policy_id': policy_id, + 'ext_name': self.OS_EP_POLICY_EXT} + return self._list( + url, + endpoints.EndpointManager.collection_key, + obj_class=endpoints.EndpointManager.resource_class) diff --git a/keystoneclient/v3/contrib/federation/__init__.py b/keystoneclient/v3/contrib/federation/__init__.py new file mode 100644 index 000000000..ee9bcef98 --- /dev/null +++ b/keystoneclient/v3/contrib/federation/__init__.py @@ -0,0 +1,13 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.v3.contrib.federation.core import * # noqa diff --git a/keystoneclient/v3/contrib/federation/base.py b/keystoneclient/v3/contrib/federation/base.py new file mode 100644 index 000000000..c09bc2267 --- /dev/null +++ b/keystoneclient/v3/contrib/federation/base.py @@ -0,0 +1,39 @@ +# 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 keystoneauth1 import exceptions +from keystoneauth1 import plugin + +from keystoneclient import base + + +class EntityManager(base.Manager, metaclass=abc.ABCMeta): + """Manager class for listing federated accessible objects.""" + + resource_class = None + + @property + @abc.abstractmethod + def object_type(self): + raise exceptions.MethodNotImplemented + + def list(self): + url = '/auth/%s' % self.object_type + try: + tenant_list = self._list(url, self.object_type) + except exceptions.CatalogException: + endpoint_filter = {'interface': plugin.AUTH_INTERFACE} + tenant_list = self._list(url, self.object_type, + endpoint_filter=endpoint_filter) + return tenant_list diff --git a/keystoneclient/v3/contrib/federation/core.py b/keystoneclient/v3/contrib/federation/core.py new file mode 100644 index 000000000..2e12cf607 --- /dev/null +++ b/keystoneclient/v3/contrib/federation/core.py @@ -0,0 +1,31 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.v3.contrib.federation import domains +from keystoneclient.v3.contrib.federation import identity_providers +from keystoneclient.v3.contrib.federation import mappings +from keystoneclient.v3.contrib.federation import projects +from keystoneclient.v3.contrib.federation import protocols +from keystoneclient.v3.contrib.federation import saml +from keystoneclient.v3.contrib.federation import service_providers + + +class FederationManager(object): + def __init__(self, api): + self.identity_providers = identity_providers.IdentityProviderManager( + api) + self.mappings = mappings.MappingManager(api) + self.protocols = protocols.ProtocolManager(api) + self.projects = projects.ProjectManager(api) + self.domains = domains.DomainManager(api) + self.saml = saml.SamlManager(api) + self.service_providers = service_providers.ServiceProviderManager(api) diff --git a/keystoneclient/v3/contrib/federation/domains.py b/keystoneclient/v3/contrib/federation/domains.py new file mode 100644 index 000000000..4d5e1353d --- /dev/null +++ b/keystoneclient/v3/contrib/federation/domains.py @@ -0,0 +1,19 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.v3.contrib.federation import base as federation_base +from keystoneclient.v3 import domains + + +class DomainManager(federation_base.EntityManager): + object_type = 'domains' + resource_class = domains.Domain diff --git a/keystoneclient/v3/contrib/federation/identity_providers.py b/keystoneclient/v3/contrib/federation/identity_providers.py new file mode 100644 index 000000000..221ec11f4 --- /dev/null +++ b/keystoneclient/v3/contrib/federation/identity_providers.py @@ -0,0 +1,110 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class IdentityProvider(base.Resource): + """Object representing Identity Provider container. + + Attributes: + * id: user-defined unique string identifying Identity Provider. + + """ + + pass + + +class IdentityProviderManager(base.CrudManager): + """Manager class for manipulating Identity Providers.""" + + resource_class = IdentityProvider + collection_key = 'identity_providers' + key = 'identity_provider' + base_url = 'OS-FEDERATION' + + def _build_url_and_put(self, **kwargs): + url = self.build_url(dict_args_in_out=kwargs) + body = {self.key: kwargs} + return self._update(url, body=body, response_key=self.key, + method='PUT') + + def create(self, id, **kwargs): + """Create Identity Provider object. + + Utilize Keystone URI: + PUT /OS-FEDERATION/identity_providers/$identity_provider + + :param id: unique id of the identity provider. + :param kwargs: optional attributes: description (str), domain_id (str), + enabled (boolean) and remote_ids (list). + :returns: an IdentityProvider resource object. + :rtype: :py:class:`keystoneclient.v3.federation.IdentityProvider` + + """ + return self._build_url_and_put(identity_provider_id=id, + **kwargs) + + def get(self, identity_provider): + """Fetch Identity Provider object. + + Utilize Keystone URI: + GET /OS-FEDERATION/identity_providers/$identity_provider + + :param identity_provider: an object with identity_provider_id + stored inside. + :returns: an IdentityProvider resource object. + :rtype: :py:class:`keystoneclient.v3.federation.IdentityProvider` + + """ + return super(IdentityProviderManager, self).get( + identity_provider_id=base.getid(identity_provider)) + + def list(self, **kwargs): + """List all Identity Providers. + + Utilize Keystone URI: + GET /OS-FEDERATION/identity_providers + + :returns: a list of IdentityProvider resource objects. + :rtype: List + + """ + return super(IdentityProviderManager, self).list(**kwargs) + + def update(self, identity_provider, **kwargs): + """Update Identity Provider object. + + Utilize Keystone URI: + PATCH /OS-FEDERATION/identity_providers/$identity_provider + + :param identity_provider: an object with identity_provider_id + stored inside. + :returns: an IdentityProvider resource object. + :rtype: :py:class:`keystoneclient.v3.federation.IdentityProvider` + + """ + return super(IdentityProviderManager, self).update( + identity_provider_id=base.getid(identity_provider), **kwargs) + + def delete(self, identity_provider): + """Delete Identity Provider object. + + Utilize Keystone URI: + DELETE /OS-FEDERATION/identity_providers/$identity_provider + + :param identity_provider: the Identity Provider ID itself or an object + with it stored inside. + + """ + return super(IdentityProviderManager, self).delete( + identity_provider_id=base.getid(identity_provider)) diff --git a/keystoneclient/v3/contrib/federation/mappings.py b/keystoneclient/v3/contrib/federation/mappings.py new file mode 100644 index 000000000..a0e54ae6f --- /dev/null +++ b/keystoneclient/v3/contrib/federation/mappings.py @@ -0,0 +1,150 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Mapping(base.Resource): + """An object representing mapping container. + + Attributes: + * id: user defined unique string identifying mapping. + + """ + + pass + + +class MappingManager(base.CrudManager): + """Manager class for manipulating federation mappings.""" + + resource_class = Mapping + collection_key = 'mappings' + key = 'mapping' + base_url = 'OS-FEDERATION' + + def _build_url_and_put(self, **kwargs): + url = self.build_url(dict_args_in_out=kwargs) + body = {self.key: kwargs} + return self._update(url, body=body, + response_key=self.key, + method='PUT') + + def create(self, mapping_id, **kwargs): + """Create federation mapping. + + Utilize Identity API operation: + PUT /OS-FEDERATION/mappings/$mapping_id + + :param mapping_id: user defined string identifier of the federation + mapping. + :param rules: a list of mapping rules. + + Example of the ``rules`` parameter:: + + [ + { + "local": [ + { + "group": { + "id": "0cd5e9" + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "not_any_of": [ + "Contractor", + "Guest" + ] + } + ] + } + ] + + """ + return self._build_url_and_put( + mapping_id=mapping_id, **kwargs) + + def get(self, mapping): + """Fetch federation mapping identified by mapping id. + + Utilize Identity API operation: + GET /OS-FEDERATION/mappings/$mapping_id + + :param mapping: a Mapping type object with mapping id + stored inside. + + """ + return super(MappingManager, self).get( + mapping_id=base.getid(mapping)) + + def list(self, **kwargs): + """List all federation mappings. + + Utilize Identity API operation: + GET /OS-FEDERATION/mappings + + """ + return super(MappingManager, self).list(**kwargs) + + def update(self, mapping, **kwargs): + """Update federation mapping identified by mapping id. + + Utilize Identity API operation: + PATCH /OS-FEDERATION/mappings/$mapping_id + + :param mapping: a Mapping type object with mapping id + stored inside. + :param rules: a list of mapping rules. + + Example of the ``rules`` parameter:: + + + [ + { + "local": [ + { + "group": { + "id": "0cd5e9" + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "not_any_of": [ + "Contractor", + "Guest" + ] + } + ] + } + ] + + """ + return super(MappingManager, self).update( + mapping_id=base.getid(mapping), **kwargs) + + def delete(self, mapping): + """Delete federation mapping identified by mapping id. + + Utilize Identity API operation: + DELETE /OS-FEDERATION/mappings/$mapping_id + + :param mapping: a Mapping type object with mapping id + stored inside. + + """ + return super(MappingManager, self).delete( + mapping_id=base.getid(mapping)) diff --git a/keystoneclient/v3/contrib/federation/projects.py b/keystoneclient/v3/contrib/federation/projects.py new file mode 100644 index 000000000..8e657b454 --- /dev/null +++ b/keystoneclient/v3/contrib/federation/projects.py @@ -0,0 +1,19 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.v3.contrib.federation import base as federation_base +from keystoneclient.v3 import projects + + +class ProjectManager(federation_base.EntityManager): + object_type = 'projects' + resource_class = projects.Project diff --git a/keystoneclient/v3/contrib/federation/protocols.py b/keystoneclient/v3/contrib/federation/protocols.py new file mode 100644 index 000000000..4fad689fe --- /dev/null +++ b/keystoneclient/v3/contrib/federation/protocols.py @@ -0,0 +1,143 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Protocol(base.Resource): + """An object representing federation protocol container. + + Attributes: + * id: user-defined unique per Identity Provider string identifying + federation protocol. + + """ + + pass + + +class ProtocolManager(base.CrudManager): + """Manager class for manipulating federation protocols.""" + + resource_class = Protocol + collection_key = 'protocols' + key = 'protocol' + base_url = 'OS-FEDERATION/identity_providers' + + def build_url(self, dict_args_in_out=None): + """Build URL for federation protocols.""" + if dict_args_in_out is None: + dict_args_in_out = {} + + identity_provider_id = dict_args_in_out.pop('identity_provider_id', + None) + if identity_provider_id: + base_url = '/'.join([self.base_url, identity_provider_id]) + else: + base_url = self.base_url + + dict_args_in_out.setdefault('base_url', base_url) + return super(ProtocolManager, self).build_url(dict_args_in_out) + + def _build_url_and_put(self, request_body=None, **kwargs): + url = self.build_url(dict_args_in_out=kwargs) + body = {self.key: request_body} + return self._update(url, body=body, + response_key=self.key, + method='PUT') + + def create(self, protocol_id, identity_provider, mapping, **kwargs): + """Create federation protocol object and tie to the Identity Provider. + + Utilize Identity API operation: + PUT /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + :param protocol_id: a string type parameter identifying a federation + protocol + :param identity_provider: a string type parameter identifying an + Identity Provider + :param mapping: a base.Resource object with federation mapping id + + """ + return self._build_url_and_put( + request_body={'mapping_id': base.getid(mapping)}, + identity_provider_id=base.getid(identity_provider), + protocol_id=protocol_id, **kwargs) + + def get(self, identity_provider, protocol, **kwargs): + """Fetch federation protocol object tied to the Identity Provider. + + Utilize Identity API operation: + GET /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + :param identity_provider: a base.Resource type object with Identity + Provider id stored inside + :param protocol: a base.Resource type object with federation protocol + id stored inside + + """ + return super(ProtocolManager, self).get( + identity_provider_id=base.getid(identity_provider), + protocol_id=base.getid(protocol), **kwargs) + + def list(self, identity_provider, **kwargs): + """List all federation protocol objects tied to the Identity Provider. + + Utilize Identity API operation: + GET /OS-FEDERATION/identity_providers/ + $identity_provider/protocols + + :param identity_provider: a base.Resource type object with Identity + Provider id stored inside + + """ + return super(ProtocolManager, self).list( + identity_provider_id=base.getid(identity_provider), **kwargs) + + def update(self, identity_provider, protocol, mapping, **kwargs): + """Update Protocol object tied to the Identity Provider. + + Utilize Identity API operation: + PATCH /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + :param identity_provider: a base.Resource type object with Identity + Provider id stored inside + :param protocol: a base.Resource type object with federation protocol + id stored inside + :param mapping: a base.Resource object with federation mapping id + + + """ + return super(ProtocolManager, self).update( + identity_provider_id=base.getid(identity_provider), + protocol_id=base.getid(protocol), mapping_id=base.getid(mapping), + **kwargs) + + def delete(self, identity_provider, protocol): + """Delete Protocol object tied to the Identity Provider. + + Utilize Identity API operation: + DELETE /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + :param identity_provider: a base.Resource type object with + Identity Provider id stored inside + :param protocol: a base.Resource type object with federation + protocol id stored inside + + """ + return super(ProtocolManager, self).delete( + identity_provider_id=base.getid(identity_provider), + protocol_id=base.getid(protocol)) diff --git a/keystoneclient/v3/contrib/federation/saml.py b/keystoneclient/v3/contrib/federation/saml.py new file mode 100644 index 000000000..435e45dc6 --- /dev/null +++ b/keystoneclient/v3/contrib/federation/saml.py @@ -0,0 +1,79 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +SAML2_ENDPOINT = '/auth/OS-FEDERATION/saml2' +ECP_ENDPOINT = '/auth/OS-FEDERATION/saml2/ecp' + + +class SamlManager(base.Manager): + """Manager class for creating SAML assertions.""" + + def create_saml_assertion(self, service_provider, token_id): + """Create a SAML assertion from a token. + + Equivalent Identity API call: + POST /auth/OS-FEDERATION/saml2 + + :param service_provider: Service Provider resource. + :type service_provider: string + :param token_id: Token to transform to SAML assertion. + :type token_id: string + + :returns: SAML representation of token_id + :rtype: string + """ + headers, body = self._create_common_request(service_provider, token_id) + resp, body = self.client.post(SAML2_ENDPOINT, json=body, + headers=headers) + return self._prepare_return_value(resp, resp.text) + + def create_ecp_assertion(self, service_provider, token_id): + """Create an ECP wrapped SAML assertion from a token. + + Equivalent Identity API call: + POST /auth/OS-FEDERATION/saml2/ecp + + :param service_provider: Service Provider resource. + :type service_provider: string + :param token_id: Token to transform to SAML assertion. + :type token_id: string + + :returns: SAML representation of token_id, wrapped in ECP envelope + :rtype: string + """ + headers, body = self._create_common_request(service_provider, token_id) + resp, body = self.client.post(ECP_ENDPOINT, json=body, + headers=headers) + return self._prepare_return_value(resp, resp.text) + + def _create_common_request(self, service_provider, token_id): + headers = {'Content-Type': 'application/json'} + body = { + 'auth': { + 'identity': { + 'methods': ['token'], + 'token': { + 'id': token_id + } + }, + 'scope': { + 'service_provider': { + 'id': base.getid(service_provider) + } + } + } + } + + return (headers, body) diff --git a/keystoneclient/v3/contrib/federation/service_providers.py b/keystoneclient/v3/contrib/federation/service_providers.py new file mode 100644 index 000000000..fed1257d1 --- /dev/null +++ b/keystoneclient/v3/contrib/federation/service_providers.py @@ -0,0 +1,103 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class ServiceProvider(base.Resource): + """Object representing Service Provider container. + + Attributes: + * id: user-defined unique string identifying Service Provider. + * sp_url: the shibboleth endpoint of a Service Provider. + * auth_url: the authentication url of Service Provider. + + """ + + pass + + +class ServiceProviderManager(base.CrudManager): + """Manager class for manipulating Service Providers.""" + + resource_class = ServiceProvider + collection_key = 'service_providers' + key = 'service_provider' + base_url = 'OS-FEDERATION' + + def _build_url_and_put(self, **kwargs): + url = self.build_url(dict_args_in_out=kwargs) + body = {self.key: kwargs} + return self._update(url, body=body, response_key=self.key, + method='PUT') + + def create(self, id, **kwargs): + """Create Service Provider object. + + Utilize Keystone URI: + ``PUT /OS-FEDERATION/service_providers/{id}`` + + :param id: unique id of the service provider. + + """ + return self._build_url_and_put(service_provider_id=id, + **kwargs) + + def get(self, service_provider): + """Fetch Service Provider object. + + Utilize Keystone URI: + ``GET /OS-FEDERATION/service_providers/{id}`` + + :param service_provider: an object with service_provider_id + stored inside. + + """ + return super(ServiceProviderManager, self).get( + service_provider_id=base.getid(service_provider)) + + def list(self, **kwargs): + """List all Service Providers. + + Utilize Keystone URI: + ``GET /OS-FEDERATION/service_providers`` + + """ + return super(ServiceProviderManager, self).list(**kwargs) + + def update(self, service_provider, **kwargs): + """Update the existing Service Provider object on the server. + + Only properties provided to the function are being updated. + + Utilize Keystone URI: + ``PATCH /OS-FEDERATION/service_providers/{id}`` + + :param service_provider: an object with service_provider_id + stored inside. + + """ + return super(ServiceProviderManager, self).update( + service_provider_id=base.getid(service_provider), **kwargs) + + def delete(self, service_provider): + """Delete Service Provider object. + + Utilize Keystone URI: + ``DELETE /OS-FEDERATION/service_providers/{id}`` + + :param service_provider: an object with service_provider_id + stored inside. + + """ + return super(ServiceProviderManager, self).delete( + service_provider_id=base.getid(service_provider)) diff --git a/keystoneclient/v3/contrib/oauth1/__init__.py b/keystoneclient/v3/contrib/oauth1/__init__.py new file mode 100644 index 000000000..e1fb8b797 --- /dev/null +++ b/keystoneclient/v3/contrib/oauth1/__init__.py @@ -0,0 +1,13 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.v3.contrib.oauth1.core import * # noqa diff --git a/keystoneclient/v3/contrib/oauth1/access_tokens.py b/keystoneclient/v3/contrib/oauth1/access_tokens.py new file mode 100644 index 000000000..a64c5dddc --- /dev/null +++ b/keystoneclient/v3/contrib/oauth1/access_tokens.py @@ -0,0 +1,50 @@ +# 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 + +from keystoneclient import base +from keystoneclient.v3.contrib.oauth1 import utils + +try: + from oauthlib import oauth1 +except ImportError: + oauth1 = None + + +class AccessToken(base.Resource): + pass + + +class AccessTokenManager(base.CrudManager): + """Manager class for manipulating identity OAuth access tokens.""" + + resource_class = AccessToken + + def create(self, consumer_key, consumer_secret, request_key, + request_secret, verifier): + endpoint = utils.OAUTH_PATH + '/access_token' + oauth_client = oauth1.Client(consumer_key, + client_secret=consumer_secret, + resource_owner_key=request_key, + resource_owner_secret=request_secret, + signature_method=oauth1.SIGNATURE_HMAC, + verifier=verifier) + url = self.client.get_endpoint(interface=plugin.AUTH_INTERFACE).rstrip( + '/') + url, headers, body = oauth_client.sign(url + endpoint, + http_method='POST') + resp, body = self.client.post(endpoint, headers=headers) + token = utils.get_oauth_token_from_body(resp.content) + return self._prepare_return_value(resp, + self.resource_class(self, token)) diff --git a/keystoneclient/v3/contrib/oauth1/auth.py b/keystoneclient/v3/contrib/oauth1/auth.py new file mode 100644 index 000000000..bd4a152e2 --- /dev/null +++ b/keystoneclient/v3/contrib/oauth1/auth.py @@ -0,0 +1,55 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from keystoneclient.auth.identity import v3 + +try: + from oauthlib import oauth1 +except ImportError: + oauth1 = None + + +class OAuthMethod(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 __init__(self, **kwargs): + super(OAuthMethod, self).__init__(**kwargs) + if oauth1 is None: + raise NotImplementedError('optional package oauthlib' + ' is not installed') + + 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', {} + + +class OAuth(v3.AuthConstructor): + _auth_method_class = OAuthMethod diff --git a/keystoneclient/v3/contrib/oauth1/consumers.py b/keystoneclient/v3/contrib/oauth1/consumers.py new file mode 100644 index 000000000..5c81ff47f --- /dev/null +++ b/keystoneclient/v3/contrib/oauth1/consumers.py @@ -0,0 +1,54 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from keystoneclient import base +from keystoneclient.v3.contrib.oauth1 import utils + + +class Consumer(base.Resource): + """Represents an OAuth consumer. + + Attributes: + * id: a uuid that identifies the consumer + * description: a short description of the consumer + """ + + pass + + +class ConsumerManager(base.CrudManager): + """Manager class for manipulating identity consumers.""" + + resource_class = Consumer + collection_key = 'consumers' + key = 'consumer' + base_url = utils.OAUTH_PATH + + def create(self, description=None, **kwargs): + return super(ConsumerManager, self).create( + description=description, + **kwargs) + + def get(self, consumer): + return super(ConsumerManager, self).get( + consumer_id=base.getid(consumer)) + + def update(self, consumer, description=None, **kwargs): + return super(ConsumerManager, self).update( + consumer_id=base.getid(consumer), + description=description, + **kwargs) + + def delete(self, consumer): + return super(ConsumerManager, self).delete( + consumer_id=base.getid(consumer)) diff --git a/keystoneclient/v3/contrib/oauth1/core.py b/keystoneclient/v3/contrib/oauth1/core.py new file mode 100644 index 000000000..4b0278e13 --- /dev/null +++ b/keystoneclient/v3/contrib/oauth1/core.py @@ -0,0 +1,66 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from keystoneclient.i18n import _ +from keystoneclient.v3.contrib.oauth1 import access_tokens +from keystoneclient.v3.contrib.oauth1 import consumers +from keystoneclient.v3.contrib.oauth1 import request_tokens + + +def create_oauth_manager(self): + # NOTE(stevemar): Attempt to import the oauthlib package at this point. + try: + import oauthlib # noqa + # NOTE(stevemar): Return an object instead of raising an exception here, + # this will allow users to see an exception only when trying to access the + # oauth portions of client. Otherwise an exception would be raised + # when the client is created. + except ImportError: + return OAuthManagerOptionalImportProxy() + else: + return OAuthManager(self) + + +class OAuthManager(object): + def __init__(self, api): + self.access_tokens = access_tokens.AccessTokenManager(api) + self.consumers = consumers.ConsumerManager(api) + self.request_tokens = request_tokens.RequestTokenManager(api) + + +class OAuthManagerOptionalImportProxy(object): + """Act as a proxy manager in case oauthlib is no installed. + + This class will only be created if oauthlib is not in the system, + trying to access any of the attributes in name (access_tokens, + consumers, request_tokens), will result in a NotImplementedError, + and a message. + + >>> manager.access_tokens.blah + NotImplementedError: To use 'access_tokens' oauthlib must be installed + + Otherwise, if trying to access an attribute other than the ones in name, + the manager will state that the attribute does not exist. + + >>> manager.dne.blah + AttributeError: 'OAuthManagerOptionalImportProxy' object has no + attribute 'dne' + """ + + def __getattribute__(self, name): + """Return error when name is related to oauthlib and not exist.""" + if name in ('access_tokens', 'consumers', 'request_tokens'): + raise NotImplementedError( + _('To use %r oauthlib must be installed') % name) + return super(OAuthManagerOptionalImportProxy, + self).__getattribute__(name) diff --git a/keystoneclient/v3/contrib/oauth1/request_tokens.py b/keystoneclient/v3/contrib/oauth1/request_tokens.py new file mode 100644 index 000000000..7494e8349 --- /dev/null +++ b/keystoneclient/v3/contrib/oauth1/request_tokens.py @@ -0,0 +1,73 @@ +# 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 urllib.parse as urlparse + +from keystoneauth1 import plugin + +from keystoneclient import base +from keystoneclient.v3.contrib.oauth1 import utils + +try: + from oauthlib import oauth1 +except ImportError: + oauth1 = None + + +class RequestToken(base.Resource): + def authorize(self, roles): + try: + retval = self.manager.authorize(self.id, roles) + self = retval + except Exception: + retval = None + + return retval + + +class RequestTokenManager(base.CrudManager): + """Manager class for manipulating identity OAuth request tokens.""" + + resource_class = RequestToken + + def authorize(self, request_token, roles): + """Authorize a request token with specific roles. + + Utilize Identity API operation: + PUT /OS-OAUTH1/authorize/$request_token_id + + :param request_token: a request token that will be authorized, and + can be exchanged for an access token. + :param roles: a list of roles, that will be delegated to the user. + """ + request_id = urlparse.quote(base.getid(request_token)) + endpoint = utils.OAUTH_PATH + '/authorize/%s' % (request_id) + body = {'roles': [{'id': base.getid(r_id)} for r_id in roles]} + return self._put(endpoint, body, "token") + + def create(self, consumer_key, consumer_secret, project): + endpoint = utils.OAUTH_PATH + '/request_token' + headers = {'requested-project-id': base.getid(project)} + oauth_client = oauth1.Client(consumer_key, + client_secret=consumer_secret, + signature_method=oauth1.SIGNATURE_HMAC, + callback_uri="oob") + url = self.client.get_endpoint(interface=plugin.AUTH_INTERFACE).rstrip( + "/") + url, headers, body = oauth_client.sign(url + endpoint, + http_method='POST', + headers=headers) + resp, body = self.client.post(endpoint, headers=headers) + token = utils.get_oauth_token_from_body(resp.content) + return self._prepare_return_value(resp, + self.resource_class(self, token)) diff --git a/keystoneclient/v3/contrib/oauth1/utils.py b/keystoneclient/v3/contrib/oauth1/utils.py new file mode 100644 index 000000000..40a0632af --- /dev/null +++ b/keystoneclient/v3/contrib/oauth1/utils.py @@ -0,0 +1,36 @@ +# 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 urllib.parse as urlparse + + +OAUTH_PATH = '/OS-OAUTH1' + + +def get_oauth_token_from_body(body): + """Parse the URL response body to retrieve the oauth token key and secret. + + The response body will look like: + 'oauth_token=12345&oauth_token_secret=67890' with + 'oauth_expires_at=2013-03-30T05:27:19.463201' possibly there, too. + """ + body = body.decode('utf-8') + + credentials = urlparse.parse_qs(body) + key = credentials['oauth_token'][0] + secret = credentials['oauth_token_secret'][0] + token = {'key': key, 'id': key, 'secret': secret} + expires_at = credentials.get('oauth_expires_at') + if expires_at: + token['expires'] = expires_at[0] + return token diff --git a/keystoneclient/v3/contrib/simple_cert.py b/keystoneclient/v3/contrib/simple_cert.py new file mode 100644 index 000000000..6b58b27b3 --- /dev/null +++ b/keystoneclient/v3/contrib/simple_cert.py @@ -0,0 +1,44 @@ +# Copyright 2014 IBM Corp. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class SimpleCertManager(object): + """Manager for the OS-SIMPLE-CERT extension.""" + + def __init__(self, client): + self._client = client + self.mgr = base.Manager(self._client) + + def get_ca_certificates(self): + """Get CA certificates. + + :returns: PEM-formatted string. + :rtype: str + + """ + resp, body = self._client.get('/OS-SIMPLE-CERT/ca', + authenticated=False) + return self.mgr._prepare_return_value(resp, resp.text) + + def get_certificates(self): + """Get signing certificates. + + :returns: PEM-formatted string. + :rtype: str + + """ + resp, body = self._client.get('/OS-SIMPLE-CERT/certificates', + authenticated=False) + return self.mgr._prepare_return_value(resp, resp.text) diff --git a/keystoneclient/v3/contrib/trusts.py b/keystoneclient/v3/contrib/trusts.py new file mode 100644 index 000000000..a8ef57909 --- /dev/null +++ b/keystoneclient/v3/contrib/trusts.py @@ -0,0 +1,102 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ +from keystoneclient import utils + + +class Trust(base.Resource): + """Represents a Trust. + + Attributes: + * id: a uuid that identifies the trust + * impersonation: allow explicit impersonation + * project_id: project ID + * trustee_user_id: a uuid that identifies the trustee + * trustor_user_id: a uuid that identifies the trustor + """ + + pass + + +class TrustManager(base.CrudManager): + """Manager class for manipulating Trusts.""" + + resource_class = Trust + collection_key = 'trusts' + key = 'trust' + base_url = '/OS-TRUST' + + def create(self, trustee_user, trustor_user, role_names=None, + role_ids=None, project=None, impersonation=False, + expires_at=None, remaining_uses=None, **kwargs): + """Create a Trust. + + :param string trustee_user: user who is capable of consuming the trust + :param string trustor_user: user who's authorization is being delegated + :param string role_names: subset of trustor's roles to be granted + :param string role_ids: subset of trustor's roles to be granted + :param string project: project which the trustor is delegating + :param boolean impersonation: enable explicit impersonation + :param datetime.datetime expires_at: expiry time + :param integer remaining_uses: how many times this trust can be used + to generate a token. None means + unlimited tokens. + + """ + # Convert role_names list into list-of-dict API format + roles = [] + if role_names: + roles.extend([{'name': n} for n in role_names]) + if role_ids: + roles.extend([{'id': i} for i in role_ids]) + + if not roles: + roles = None + + # Convert datetime.datetime expires_at to iso format string + if expires_at: + expires_str = utils.isotime(at=expires_at, subsecond=True) + else: + expires_str = None + + return super(TrustManager, self).create( + expires_at=expires_str, + impersonation=impersonation, + project_id=base.getid(project), + remaining_uses=remaining_uses, + roles=roles, + trustee_user_id=base.getid(trustee_user), + trustor_user_id=base.getid(trustor_user), + **kwargs) + + def update(self): + raise exceptions.MethodNotImplemented( + _('Update not supported for trusts')) + + def list(self, trustee_user=None, trustor_user=None, **kwargs): + """List Trusts.""" + trustee_user_id = base.getid(trustee_user) + trustor_user_id = base.getid(trustor_user) + return super(TrustManager, self).list(trustee_user_id=trustee_user_id, + trustor_user_id=trustor_user_id, + **kwargs) + + def get(self, trust): + """Get a specific trust.""" + return super(TrustManager, self).get(trust_id=base.getid(trust)) + + def delete(self, trust): + """Delete a trust.""" + return super(TrustManager, self).delete(trust_id=base.getid(trust)) diff --git a/keystoneclient/v3/credentials.py b/keystoneclient/v3/credentials.py index 264c367e7..70e067001 100644 --- a/keystoneclient/v3/credentials.py +++ b/keystoneclient/v3/credentials.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -22,36 +22,118 @@ class Credential(base.Resource): Attributes: * id: a uuid that identifies the credential + * user_id: user ID to which credential belongs + * type: the type of credential + * blob: the text that represents the credential + * project_id: project ID which limits the scope of the credential """ + pass class CredentialManager(base.CrudManager): """Manager class for manipulating Identity credentials.""" + resource_class = Credential collection_key = 'credentials' key = 'credential' - def create(self, user, type, data, project=None): + def create(self, user, type, blob, project=None, **kwargs): + """Create a credential. + + :param user: the user to which the credential belongs + :type user: str or :class:`keystoneclient.v3.users.User` + :param str type: the type of the credential, valid values are: + ``ec2``, ``cert`` or ``totp`` + :param str blob: the arbitrary blob of the credential data, to be + parsed according to the type + :param project: the project which limits the scope of the credential, + this attribbute is mandatory if the credential type is + ec2 + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param kwargs: any other attribute provided will be passed to the + server + + :returns: the created credential + :rtype: :class:`keystoneclient.v3.credentials.Credential` + + """ return super(CredentialManager, self).create( user_id=base.getid(user), type=type, - data=data, - project_id=base.getid(project)) + blob=blob, + project_id=base.getid(project), + **kwargs) def get(self, credential): + """Retrieve a credential. + + :param credential: the credential to be retrieved from the server + :type credential: str or + :class:`keystoneclient.v3.credentials.Credential` + + :returns: the specified credential + :rtype: :class:`keystoneclient.v3.credentials.Credential` + + """ return super(CredentialManager, self).get( credential_id=base.getid(credential)) - def update(self, credential, user, type=None, data=None, project=None): + def list(self, **kwargs): + """List credentials. + + :param kwargs: If user_id or type is specified then credentials + will be filtered accordingly. + + :returns: a list of credentials + :rtype: list of :class:`keystoneclient.v3.credentials.Credential` + + """ + return super(CredentialManager, self).list(**kwargs) + + def update(self, credential, user, type=None, blob=None, project=None, + **kwargs): + """Update a credential. + + :param credential: the credential to be updated on the server + :type credential: str or + :class:`keystoneclient.v3.credentials.Credential` + :param user: the new user to which the credential belongs + :type user: str or :class:`keystoneclient.v3.users.User` + :param str type: the new type of the credential, valid values are: + ``ec2``, ``cert`` or ``totp`` + :param str blob: the new blob of the credential data + and may be removed in the future release. + :param project: the new project which limits the scope of the + credential, this attribute is mandatory if the + credential type is ec2 + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param kwargs: any other attribute provided will be passed to the + server + + :returns: the updated credential + :rtype: :class:`keystoneclient.v3.credentials.Credential` + + """ return super(CredentialManager, self).update( credential_id=base.getid(credential), user_id=base.getid(user), type=type, - data=data, - project_id=base.getid(project)) + blob=blob, + project_id=base.getid(project), + **kwargs) def delete(self, credential): + """Delete a credential. + + :param credential: the credential to be deleted + :type credential: str or + :class:`keystoneclient.v3.credentials.Credential` + + :returns: response object with 204 status + :rtype: :class:`requests.models.Response` + + """ return super(CredentialManager, self).delete( credential_id=base.getid(credential)) diff --git a/keystoneclient/v3/domain_configs.py b/keystoneclient/v3/domain_configs.py new file mode 100644 index 000000000..4c011bce8 --- /dev/null +++ b/keystoneclient/v3/domain_configs.py @@ -0,0 +1,130 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ + + +class DomainConfig(base.Resource): + """An object representing a domain config association. + + This resource object does not necessarily contain fixed attributes, as new + attributes are added in the server, they are supported here directly. + The currently supported configs are `identity` and `ldap`. + + """ + + pass + + +class DomainConfigManager(base.Manager): + """Manager class for manipulating domain config associations.""" + + resource_class = DomainConfig + key = 'config' + + def build_url(self, domain): + return '/domains/%s/config' % base.getid(domain) + + def create(self, domain, config): + """Create a config for a domain. + + :param domain: the domain where the config is going to be applied. + :type domain: str or :py:class:`keystoneclient.v3.domains.Domain` + + :param dict config: a dictionary of domain configurations. + + Example of the ``config`` parameter:: + + { + "identity": { + "driver": "ldap" + }, + "ldap": { + "url": "ldap://myldap.com:389/", + "user_tree_dn": "ou=Users,dc=my_new_root,dc=org" + } + } + + :returns: the created domain config returned from server. + :rtype: :class:`keystoneclient.v3.domain_configs.DomainConfig` + + """ + base_url = self.build_url(domain) + body = {self.key: config} + return super(DomainConfigManager, self)._put( + base_url, body=body, response_key=self.key) + + def get(self, domain): + """Get a config for a domain. + + :param domain: the domain for which the config is defined. + :type domain: str or :py:class:`keystoneclient.v3.domains.Domain` + + :returns: the domain config returned from server. + :rtype: :class:`keystoneclient.v3.domain_configs.DomainConfig` + + """ + base_url = self.build_url(domain) + return super(DomainConfigManager, self)._get(base_url, self.key) + + def update(self, domain, config): + """Update a config for a domain. + + :param domain: the domain where the config is going to be updated. + :type domain: str or :py:class:`keystoneclient.v3.domains.Domain` + + :param dict config: a dictionary of domain configurations. + + Example of the ``config`` parameter:: + + { + "identity": { + "driver": "ldap" + }, + "ldap": { + "url": "ldap://myldap.com:389/", + "user_tree_dn": "ou=Users,dc=my_new_root,dc=org" + } + } + + :returns: the updated domain config returned from server. + :rtype: :class:`keystoneclient.v3.domain_configs.DomainConfig` + + """ + base_url = self.build_url(domain) + body = {self.key: config} + return super(DomainConfigManager, self)._patch( + base_url, body=body, response_key=self.key) + + def delete(self, domain): + """Delete a config for a domain. + + :param domain: the domain which the config will be deleted on + the server. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ + base_url = self.build_url(domain) + return super(DomainConfigManager, self)._delete(url=base_url) + + def find(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('Find not supported for domain configs')) + + def list(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('List not supported for domain configs')) diff --git a/keystoneclient/v3/domains.py b/keystoneclient/v3/domains.py index 2d27db225..0f542b8b0 100644 --- a/keystoneclient/v3/domains.py +++ b/keystoneclient/v3/domains.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -22,34 +22,102 @@ class Domain(base.Resource): Attributes: * id: a uuid that identifies the domain + * name: the name of the domain + * description: a description of the domain + * enabled: determines whether the domain is enabled """ + pass class DomainManager(base.CrudManager): """Manager class for manipulating Identity domains.""" + resource_class = Domain collection_key = 'domains' key = 'domain' - def create(self, name, description=None, enabled=True): + def create(self, name, description=None, enabled=True, **kwargs): + """Create a domain. + + :param str name: the name of the domain. + :param str description: a description of the domain. + :param bool enabled: whether the domain is enabled. + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the created domain returned from server. + :rtype: :class:`keystoneclient.v3.domains.Domain` + + """ return super(DomainManager, self).create( name=name, description=description, - enabled=enabled) + enabled=enabled, + **kwargs) def get(self, domain): + """Retrieve a domain. + + :param domain: the domain to be retrieved from the server. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + + :returns: the specified domain returned from server. + :rtype: :class:`keystoneclient.v3.domains.Domain` + + """ return super(DomainManager, self).get( domain_id=base.getid(domain)) - def update(self, domain, name=None, description=None, enabled=None): + def list(self, **kwargs): + """List domains. + + :param kwargs: allows filter criteria to be passed where + supported by the server. + + :returns: a list of domains. + :rtype: list of :class:`keystoneclient.v3.domains.Domain`. + + """ + # Ref bug #1267530 we have to pass 0 for False to get the expected + # results on all keystone versions + if kwargs.get('enabled') is False: + kwargs['enabled'] = 0 + return super(DomainManager, self).list(**kwargs) + + def update(self, domain, name=None, + description=None, enabled=None, **kwargs): + """Update a domain. + + :param domain: the domain to be updated on the server. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param str name: the new name of the domain. + :param str description: the new description of the domain. + :param bool enabled: whether the domain is enabled. + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the updated domain returned from server. + :rtype: :class:`keystoneclient.v3.domains.Domain` + + """ return super(DomainManager, self).update( domain_id=base.getid(domain), name=name, description=description, - enabled=enabled) + enabled=enabled, + **kwargs) def delete(self, domain): + """Delete a domain. + + :param domain: the domain to be deleted on the server. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ return super(DomainManager, self).delete( domain_id=base.getid(domain)) diff --git a/keystoneclient/v3/ec2.py b/keystoneclient/v3/ec2.py new file mode 100644 index 000000000..d32fbcaa3 --- /dev/null +++ b/keystoneclient/v3/ec2.py @@ -0,0 +1,98 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class EC2(base.Resource): + """Represents an EC2 resource. + + Attributes: + * id: a string that identifies the EC2 resource. + * user_id: the ID field of a pre-existing user in the backend. + * project_id: the ID field of a pre-existing project in the backend. + * access: a string representing access key of the access/secret pair. + * secret: a string representing the secret of the access/secret pair. + + """ + + def __repr__(self): + """Return string representation of EC2 resource information.""" + return "" % self._info + + +class EC2Manager(base.ManagerWithFind): + + resource_class = EC2 + + def create(self, user_id, project_id): + """Create a new access/secret pair. + + :param user_id: the ID of the user having access/secret pair. + :type user_id: str or :class:`keystoneclient.v3.users.User` + :param project_id: the ID of the project having access/secret pair. + :type project_id: str or :class:`keystoneclient.v3.projects.Project` + + :returns: the created access/secret pair returned from server. + :rtype: :class:`keystoneclient.v3.ec2.EC2` + + """ + # NOTE(jamielennox): Yes, this uses tenant_id as a key even though we + # are in the v3 API. + return self._post('/users/%s/credentials/OS-EC2' % user_id, + body={'tenant_id': project_id}, + response_key="credential") + + def get(self, user_id, access): + """Retrieve an access/secret pair for a given access key. + + :param user_id: the ID of the user whose access/secret pair will be + retrieved from the server. + :type user_id: str or :class:`keystoneclient.v3.users.User` + :param str access: the access key whose access/secret pair will be + retrieved from the server. + + :returns: the specified access/secret pair returned from server. + :rtype: :class:`keystoneclient.v3.ec2.EC2` + + """ + url = "/users/%s/credentials/OS-EC2/%s" % (user_id, base.getid(access)) + return self._get(url, response_key="credential") + + def list(self, user_id): + """List access/secret pairs for a given user. + + :param str user_id: the ID of the user having access/secret pairs will + be listed. + + :returns: a list of access/secret pairs. + :rtype: list of :class:`keystoneclient.v3.ec2.EC2` + + """ + return self._list("/users/%s/credentials/OS-EC2" % user_id, + response_key="credentials") + + def delete(self, user_id, access): + """Delete an access/secret pair. + + :param user_id: the ID of the user whose access/secret pair will be + deleted on the server. + :type user_id: str or :class:`keystoneclient.v3.users.User` + :param str access: the access key whose access/secret pair will be + deleted on the server. + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ + return self._delete("/users/%s/credentials/OS-EC2/%s" % + (user_id, base.getid(access))) diff --git a/keystoneclient/v3/endpoint_groups.py b/keystoneclient/v3/endpoint_groups.py new file mode 100644 index 000000000..f8b47c4d6 --- /dev/null +++ b/keystoneclient/v3/endpoint_groups.py @@ -0,0 +1,136 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class EndpointGroup(base.Resource): + """Represents an identity endpoint group. + + Attributes: + * id: a UUID that identifies the endpoint group + * name: the endpoint group name + * description: the endpoint group description + * filters: representation of filters in the format of JSON that define + what endpoint entities are part of the group + + """ + + pass + + +class EndpointGroupManager(base.CrudManager): + """Manager class for Endpoint Groups.""" + + resource_class = EndpointGroup + collection_key = 'endpoint_groups' + key = 'endpoint_group' + base_url = 'OS-EP-FILTER' + + def create(self, name, filters, description=None, **kwargs): + """Create an endpoint group. + + :param str name: the name of the endpoint group. + :param str filters: representation of filters in the format of JSON + that define what endpoint entities are part of the + group. + :param str description: a description of the endpoint group. + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the created endpoint group returned from server. + :rtype: :class:`keystoneclient.v3.endpoint_groups.EndpointGroup` + + """ + return super(EndpointGroupManager, self).create( + name=name, + filters=filters, + description=description, + **kwargs) + + def get(self, endpoint_group): + """Retrieve an endpoint group. + + :param endpoint_group: the endpoint group to be retrieved from the + server. + :type endpoint_group: + str or :class:`keystoneclient.v3.endpoint_groups.EndpointGroup` + + :returns: the specified endpoint group returned from server. + :rtype: :class:`keystoneclient.v3.endpoint_groups.EndpointGroup` + + """ + return super(EndpointGroupManager, self).get( + endpoint_group_id=base.getid(endpoint_group)) + + def check(self, endpoint_group): + """Check if an endpoint group exists. + + :param endpoint_group: the endpoint group to be checked against the + server. + :type endpoint_group: + str or :class:`keystoneclient.v3.endpoint_groups.EndpointGroup` + + :returns: none if the specified endpoint group exists. + + """ + return super(EndpointGroupManager, self).head( + endpoint_group_id=base.getid(endpoint_group)) + + def list(self, **kwargs): + """List endpoint groups. + + Any parameter provided will be passed to the server. + + :returns: a list of endpoint groups. + :rtype: list of + :class:`keystoneclient.v3.endpoint_groups.EndpointGroup`. + + """ + return super(EndpointGroupManager, self).list(**kwargs) + + def update(self, endpoint_group, name=None, filters=None, + description=None, **kwargs): + """Update an endpoint group. + + :param str name: the new name of the endpoint group. + :param str filters: the new representation of filters in the format of + JSON that define what endpoint entities are part of + the group. + :param str description: the new description of the endpoint group. + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the updated endpoint group returned from server. + :rtype: :class:`keystoneclient.v3.endpoint_groups.EndpointGroup` + + """ + return super(EndpointGroupManager, self).update( + endpoint_group_id=base.getid(endpoint_group), + name=name, + filters=filters, + description=description, + **kwargs) + + def delete(self, endpoint_group): + """Delete an endpoint group. + + :param endpoint_group: the endpoint group to be deleted on the server. + :type endpoint_group: + str or :class:`keystoneclient.v3.endpoint_groups.EndpointGroup` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ + return super(EndpointGroupManager, self).delete( + endpoint_group_id=base.getid(endpoint_group)) diff --git a/keystoneclient/v3/endpoints.py b/keystoneclient/v3/endpoints.py index c13313d83..0452394a5 100644 --- a/keystoneclient/v3/endpoints.py +++ b/keystoneclient/v3/endpoints.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -15,6 +15,8 @@ # under the License. from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ VALID_INTERFACES = ['public', 'admin', 'internal'] @@ -29,49 +31,121 @@ class Endpoint(base.Resource): * region: geographic location of the endpoint * service_id: service to which the endpoint belongs * url: fully qualified service endpoint - * enabled: determines whether the endpoint appears in the catalog + * enabled: determines whether the endpoint appears in the service + catalog """ + pass class EndpointManager(base.CrudManager): """Manager class for manipulating Identity endpoints.""" + resource_class = Endpoint collection_key = 'endpoints' key = 'endpoint' def _validate_interface(self, interface): if interface is not None and interface not in VALID_INTERFACES: - msg = '"interface" must be one of: %s' - msg = msg % ', '.join(VALID_INTERFACES) - raise Exception(msg) + msg = _('"interface" must be one of: %s') + msg %= ', '.join(VALID_INTERFACES) + raise exceptions.ValidationError(msg) + + def create(self, service, url, interface=None, region=None, enabled=True, + **kwargs): + """Create an endpoint. + + :param service: the service to which the endpoint belongs. + :type service: str or :class:`keystoneclient.v3.services.Service` + :param str url: the URL of the fully qualified service endpoint. + :param str interface: the network interface of the endpoint. Valid + values are: ``public``, ``admin`` or ``internal``. + :param region: the region to which the endpoint belongs. + :type region: str or :class:`keystoneclient.v3.regions.Region` + :param bool enabled: whether the endpoint is enabled or not, + determining if it appears in the service catalog. + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the created endpoint returned from server. + :rtype: :class:`keystoneclient.v3.endpoints.Endpoint` - def create(self, service, url, name=None, interface=None, region=None, - enabled=True): + """ self._validate_interface(interface) return super(EndpointManager, self).create( service_id=base.getid(service), interface=interface, url=url, region=region, - enabled=enabled) + enabled=enabled, + **kwargs) def get(self, endpoint): + """Retrieve an endpoint. + + :param endpoint: the endpoint to be retrieved from the server. + :type endpoint: str or :class:`keystoneclient.v3.endpoints.Endpoint` + + :returns: the specified endpoint returned from server. + :rtype: :class:`keystoneclient.v3.endpoints.Endpoint` + + """ return super(EndpointManager, self).get( endpoint_id=base.getid(endpoint)) - def list(self, service=None, name=None, interface=None, region=None, - enabled=None): + def list(self, service=None, interface=None, region=None, enabled=None, + region_id=None, **kwargs): + """List endpoints. + + :param service: the service of the endpoints to be filtered on. + :type service: str or :class:`keystoneclient.v3.services.Service` + :param str interface: the network interface of the endpoints to be + filtered on. Valid values are: ``public``, + ``admin`` or ``internal``. + :param bool enabled: whether to return enabled or disabled endpoints. + :param str region_id: filter endpoints by the region_id attribute. If + both region and region_id are specified, region + takes precedence. + :param kwargs: any other attribute provided will filter endpoints on. + + :returns: a list of endpoints. + :rtype: list of :class:`keystoneclient.v3.endpoints.Endpoint` + + """ + # NOTE(lhcheng): region filter is not supported by keystone, + # region_id should be used instead. Consider removing the + # region argument in the next release. self._validate_interface(interface) return super(EndpointManager, self).list( service_id=base.getid(service), interface=interface, - region=region, - enabled=enabled) + region_id=region_id or base.getid(region), + enabled=enabled, + **kwargs) + + def update(self, endpoint, service=None, url=None, interface=None, + region=None, enabled=None, **kwargs): + """Update an endpoint. + + :param endpoint: the endpoint to be updated on the server. + :type endpoint: str or :class:`keystoneclient.v3.endpoints.Endpoint` + :param service: the new service to which the endpoint belongs. + :type service: str or :class:`keystoneclient.v3.services.Service` + :param str url: the new URL of the fully qualified service endpoint. + :param str interface: the new network interface of the endpoint. Valid + values are: ``public``, ``admin`` or ``internal``. + :param region: the new region to which the endpoint belongs. + :type region: str or :class:`keystoneclient.v3.regions.Region` + :param bool enabled: determining if the endpoint appears in the service + catalog by enabling or disabling it. + :param kwargs: any other attribute provided will be passed to the + server. - def update(self, endpoint, service=None, url=None, name=None, - interface=None, region=None, enabled=None): + :returns: the updated endpoint returned from server. + :rtype: :class:`keystoneclient.v3.endpoints.Endpoint` + + """ self._validate_interface(interface) return super(EndpointManager, self).update( endpoint_id=base.getid(endpoint), @@ -79,8 +153,18 @@ def update(self, endpoint, service=None, url=None, name=None, interface=interface, url=url, region=region, - enabled=enabled) + enabled=enabled, + **kwargs) def delete(self, endpoint): + """Delete an endpoint. + + :param endpoint: the endpoint to be deleted on the server. + :type endpoint: str or :class:`keystoneclient.v3.endpoints.Endpoint` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ return super(EndpointManager, self).delete( endpoint_id=base.getid(endpoint)) diff --git a/keystoneclient/v3/groups.py b/keystoneclient/v3/groups.py index e196dafe1..843ad0028 100644 --- a/keystoneclient/v3/groups.py +++ b/keystoneclient/v3/groups.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -26,6 +26,7 @@ class Group(base.Resource): * description: group description """ + def update(self, name=None, description=None): kwargs = { 'name': name if name is not None else self.name, @@ -45,34 +46,94 @@ def update(self, name=None, description=None): class GroupManager(base.CrudManager): """Manager class for manipulating Identity groups.""" + resource_class = Group collection_key = 'groups' key = 'group' - def create(self, name, domain=None, description=None): + def create(self, name, domain=None, description=None, **kwargs): + """Create a group. + + :param str name: the name of the group. + :param domain: the domain of the group. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param str description: a description of the group. + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the created group returned from server. + :rtype: :class:`keystoneclient.v3.groups.Group` + + """ return super(GroupManager, self).create( name=name, domain_id=base.getid(domain), - description=description) + description=description, + **kwargs) - def list(self, user=None): + def list(self, user=None, domain=None, **kwargs): + """List groups. + + :param user: the user of the groups to be filtered on. + :type user: str or :class:`keystoneclient.v3.users.User` + :param domain: the domain of the groups to be filtered on. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param kwargs: any other attribute provided will filter groups on. + + :returns: a list of groups. + :rtype: list of :class:`keystoneclient.v3.groups.Group`. + + """ if user: base_url = '/users/%s' % base.getid(user) else: base_url = None return super(GroupManager, self).list( - base_url=base_url) + base_url=base_url, + domain_id=base.getid(domain), + **kwargs) def get(self, group): + """Retrieve a group. + + :param group: the group to be retrieved from the server. + :type group: str or :class:`keystoneclient.v3.groups.Group` + + :returns: the specified group returned from server. + :rtype: :class:`keystoneclient.v3.groups.Group` + + """ return super(GroupManager, self).get( group_id=base.getid(group)) - def update(self, group, name=None, description=None): + def update(self, group, name=None, description=None, **kwargs): + """Update a group. + + :param group: the group to be updated on the server. + :type group: str or :class:`keystoneclient.v3.groups.Group` + :param str name: the new name of the group. + :param str description: the new description of the group. + :param kwargs: any other attribute provided will be passed to server. + + :returns: the updated group returned from server. + :rtype: :class:`keystoneclient.v3.groups.Group` + + """ return super(GroupManager, self).update( group_id=base.getid(group), name=name, - description=description) + description=description, + **kwargs) def delete(self, group): + """Delete a group. + + :param group: the group to be deleted on the server. + :type group: str or :class:`keystoneclient.v3.groups.Group` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ return super(GroupManager, self).delete( group_id=base.getid(group)) diff --git a/keystoneclient/v3/limits.py b/keystoneclient/v3/limits.py new file mode 100644 index 000000000..52b1b886c --- /dev/null +++ b/keystoneclient/v3/limits.py @@ -0,0 +1,150 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Limit(base.Resource): + """Represents a project limit. + + Attributes: + * id: a UUID that identifies the project limit + * service_id: a UUID that identifies the service for the limit + * region_id: a UUID that identifies the region for the limit + * project_id: a UUID that identifies the project for the limit + * resource_name: the name of the resource to limit + * resource_limit: the limit to apply to the project + * description: a description for the project limit + + """ + + pass + + +class LimitManager(base.CrudManager): + """Manager class for project limits.""" + + resource_class = Limit + collection_key = 'limits' + key = 'limit' + + def create(self, project, service, resource_name, resource_limit, + description=None, region=None, **kwargs): + """Create a project-specific limit. + + :param project: the project to create a limit for. + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param service: the service that owns the resource to limit. + :type service: str or :class:`keystoneclient.v3.services.Service` + :param resource_name: the name of the resource to limit + :type resource_name: str + :param resource_limit: the quantity of the limit + :type resource_limit: int + :param description: a description of the limit + :type description: str + :param region: region the limit applies to + :type region: str or :class:`keystoneclient.v3.regions.Region` + + :returns: a reference of the created limit + :rtype: :class:`keystoneclient.v3.limits.Limit` + + """ + limit_data = base.filter_none( + project_id=base.getid(project), + service_id=base.getid(service), + resource_name=resource_name, + resource_limit=resource_limit, + description=description, + region_id=base.getid(region), + **kwargs + ) + body = {self.collection_key: [limit_data]} + resp, body = self.client.post('/limits', body=body) + limit = body[self.collection_key].pop() + return self._prepare_return_value(resp, + self.resource_class( + self, limit)) + + def update(self, limit, project=None, service=None, resource_name=None, + resource_limit=None, description=None, **kwargs): + """Update a project-specific limit. + + :param limit: a limit to update + :param project: the project ID of the limit to update + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param resource_limit: the limit of the limit's resource to update + :type: resource_limit: int + :param description: a description of the limit + :type description: str + + :returns: a reference of the updated limit. + :rtype: :class:`keystoneclient.v3.limits.Limit` + + """ + return super(LimitManager, self).update( + limit_id=base.getid(limit), + project_id=base.getid(project), + service_id=base.getid(service), + resource_name=resource_name, + resource_limit=resource_limit, + description=description, + **kwargs + ) + + def get(self, limit): + """Retrieve a project limit. + + :param limit: + the project-specific limit to be retrieved. + :type limit: + str or :class:`keystoneclient.v3.limit.Limit` + + :returns: a project-specific limit + :rtype: :class:`keystoneclient.v3.limit.Limit` + + """ + return super(LimitManager, self).get(limit_id=base.getid(limit)) + + def list(self, service=None, region=None, resource_name=None, **kwargs): + """List project-specific limits. + + Any parameter provided will be passed to the server as a filter + + :param service: service to filter limits by + :type service: UUID or :class:`keystoneclient.v3.services.Service` + :param region: region to filter limits by + :type region: UUID or :class:`keystoneclient.v3.regions.Region` + :param resource_name: the name of the resource to filter limits by + :type resource_name: str + + :returns: a list of project-specific limits. + :rtype: list of :class:`keystoneclient.v3.limits.Limit` + + """ + return super(LimitManager, self).list( + service_id=base.getid(service), + region_id=base.getid(region), + resource_name=resource_name, + **kwargs + ) + + def delete(self, limit): + """Delete a project-specific limit. + + :param limit: the project-specific limit to be deleted. + :type limit: str or :class:`keystoneclient.v3.limit.Limit` + + :returns: Response object with 204 status + :rtype: :class:`requests.models.Response` + + """ + return super(LimitManager, self).delete(limit_id=base.getid(limit)) diff --git a/keystoneclient/v3/policies.py b/keystoneclient/v3/policies.py index b628f9a58..32de94e91 100644 --- a/keystoneclient/v3/policies.py +++ b/keystoneclient/v3/policies.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -23,9 +23,10 @@ class Policy(base.Resource): Attributes: * id: a uuid that identifies the policy * blob: a policy document (blob) - * type: the mime type of the policy blob + * type: the MIME type of the policy blob """ + def update(self, blob=None, type=None): kwargs = { 'blob': blob if blob is not None else self.blob, @@ -43,28 +44,82 @@ def update(self, blob=None, type=None): class PolicyManager(base.CrudManager): """Manager class for manipulating Identity policies.""" + resource_class = Policy collection_key = 'policies' key = 'policy' - def create(self, blob, type='application/json'): + def create(self, blob, type='application/json', **kwargs): + """Create a policy. + + :param str blob: the policy document. + :param str type: the MIME type of the policy blob. + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the created policy returned from server. + :rtype: :class:`keystoneclient.v3.policies.Policy` + + """ return super(PolicyManager, self).create( blob=blob, - type=type) + type=type, + **kwargs) def get(self, policy): + """Retrieve a policy. + + :param policy: the policy to be retrieved from the server. + :type policy: str or :class:`keystoneclient.v3.policies.Policy` + + :returns: the specified policy returned from server. + :rtype: :class:`keystoneclient.v3.policies.Policy` + + """ return super(PolicyManager, self).get( policy_id=base.getid(policy)) - def list(self): - return super(PolicyManager, self).list() + def list(self, **kwargs): + """List policies. + + :param kwargs: allows filter criteria to be passed where + supported by the server. - def update(self, entity, blob=None, type=None): + :returns: a list of policies. + :rtype: list of :class:`keystoneclient.v3.policies.Policy`. + + """ + return super(PolicyManager, self).list(**kwargs) + + def update(self, policy, blob=None, type=None, **kwargs): + """Update a policy. + + :param policy: the policy to be updated on the server. + :type policy: str or :class:`keystoneclient.v3.policies.Policy` + :param str blob: the new policy document. + :param str type: the new MIME type of the policy blob. + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the updated policy returned from server. + :rtype: :class:`keystoneclient.v3.policies.Policy` + + """ return super(PolicyManager, self).update( - policy_id=base.getid(entity), + policy_id=base.getid(policy), blob=blob, - type=type) + type=type, + **kwargs) def delete(self, policy): + """Delete a policy. + + :param policy: the policy to be deleted on the server. + :type policy: str or :class:`keystoneclient.v3.policies.Policy` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ return super(PolicyManager, self).delete( policy_id=base.getid(policy)) diff --git a/keystoneclient/v3/projects.py b/keystoneclient/v3/projects.py index bcbc4fd57..4ba94bf03 100644 --- a/keystoneclient/v3/projects.py +++ b/keystoneclient/v3/projects.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -14,7 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. +import urllib.parse + from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ class Project(base.Resource): @@ -25,8 +29,14 @@ class Project(base.Resource): * name: project name * description: project description * enabled: boolean to indicate if project is enabled + * parent_id: a uuid representing this project's parent in hierarchy + * parents: a list or a structured dict containing the parents of this + project in the hierarchy + * subtree: a list or a structured dict containing the subtree of this + project in the hierarchy """ + def update(self, name=None, description=None, enabled=None): kwargs = { 'name': name if name is not None else self.name, @@ -44,39 +54,282 @@ def update(self, name=None, description=None, enabled=None): return retval + def add_tag(self, tag): + self.manager.add_tag(self, tag) + + def update_tags(self, tags): + return self.manager.update_tags(self, tags) + + def delete_tag(self, tag): + self.manager.delete_tag(self, tag) + + def delete_all_tags(self): + return self.manager.update_tags(self, []) + + def list_tags(self): + return self.manager.list_tags(self) + + def check_tag(self, tag): + return self.manager.check_tag(self, tag) + class ProjectManager(base.CrudManager): """Manager class for manipulating Identity projects.""" + resource_class = Project collection_key = 'projects' key = 'project' - def create(self, name, domain, description=None, enabled=True): + def create(self, name, domain, description=None, + enabled=True, parent=None, **kwargs): + """Create a project. + + :param str name: the name of the project. + :param domain: the domain of the project. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param str description: the description of the project. + :param bool enabled: whether the project is enabled. + :param parent: the parent of the project in the hierarchy. + :type parent: str or :class:`keystoneclient.v3.projects.Project` + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the created project returned from server. + :rtype: :class:`keystoneclient.v3.projects.Project` + + """ + # NOTE(rodrigods): the API must be backwards compatible, so if an + # application was passing a 'parent_id' before as kwargs, the call + # should not fail. If both 'parent' and 'parent_id' are provided, + # 'parent' will be preferred. + if parent: + kwargs['parent_id'] = base.getid(parent) + return super(ProjectManager, self).create( domain_id=base.getid(domain), name=name, description=description, - enabled=enabled) + enabled=enabled, + **kwargs) + + def list(self, domain=None, user=None, parent=None, **kwargs): + """List projects. - def list(self, domain=None, user=None): + :param domain: the domain of the projects to be filtered on. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param user: filter in projects the specified user has role + assignments on. + :type user: str or :class:`keystoneclient.v3.users.User` + :param parent: filter in projects the specified project is a parent + for + :type parent: str or :class:`keystoneclient.v3.projects.Project` + :param kwargs: any other attribute provided will filter projects on. + Project tags filter keyword: ``tags``, ``tags_any``, + ``not_tags``, and ``not_tags_any``. tag attribute type + string. Pass in a comma separated string to filter + with multiple tags. + + :returns: a list of projects. + :rtype: list of :class:`keystoneclient.v3.projects.Project` + + """ base_url = '/users/%s' % base.getid(user) if user else None - return super(ProjectManager, self).list( + projects = super(ProjectManager, self).list( base_url=base_url, - domain_id=base.getid(domain)) + domain_id=base.getid(domain), + parent_id=base.getid(parent), + fallback_to_auth=True, + **kwargs) - def get(self, project): - return super(ProjectManager, self).get( - project_id=base.getid(project)) + base_response = None + list_data = projects + if self.client.include_metadata: + base_response = projects + list_data = projects.data + base_response.data = list_data + + for p in list_data: + p.tags = getattr(p, 'tags', []) + + if self.client.include_metadata: + base_response.data = list_data + + return base_response if self.client.include_metadata else list_data + + def _check_not_parents_as_ids_and_parents_as_list(self, parents_as_ids, + parents_as_list): + if parents_as_ids and parents_as_list: + msg = _('Specify either parents_as_ids or parents_as_list ' + 'parameters, not both') + raise exceptions.ValidationError(msg) + + def _check_not_subtree_as_ids_and_subtree_as_list(self, subtree_as_ids, + subtree_as_list): + if subtree_as_ids and subtree_as_list: + msg = _('Specify either subtree_as_ids or subtree_as_list ' + 'parameters, not both') + raise exceptions.ValidationError(msg) + + def get(self, project, subtree_as_list=False, parents_as_list=False, + subtree_as_ids=False, parents_as_ids=False): + """Retrieve a project. + + :param project: the project to be retrieved from the server. + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param bool subtree_as_list: retrieve projects below this project in + the hierarchy as a flat list. It only + includes the projects in which the current + user has role assignments on. + :param bool parents_as_list: retrieve projects above this project in + the hierarchy as a flat list. It only + includes the projects in which the current + user has role assignments on. + :param bool subtree_as_ids: retrieve the IDs from the projects below + this project in the hierarchy as a + structured dictionary. + :param bool parents_as_ids: retrieve the IDs from the projects above + this project in the hierarchy as a + structured dictionary. + :returns: the specified project returned from server. + :rtype: :class:`keystoneclient.v3.projects.Project` + + :raises keystoneclient.exceptions.ValidationError: if subtree_as_list + and subtree_as_ids or parents_as_list and parents_as_ids are + included at the same time in the call. + + """ + self._check_not_parents_as_ids_and_parents_as_list( + parents_as_ids, parents_as_list) + self._check_not_subtree_as_ids_and_subtree_as_list( + subtree_as_ids, subtree_as_list) + + # According to the API spec, the query params are key only + query_params = [] + if subtree_as_list: + query_params.append('subtree_as_list') + if subtree_as_ids: + query_params.append('subtree_as_ids') + if parents_as_list: + query_params.append('parents_as_list') + if parents_as_ids: + query_params.append('parents_as_ids') + + query = self.build_key_only_query(query_params) + dict_args = {'project_id': base.getid(project)} + url = self.build_url(dict_args_in_out=dict_args) + p = self._get(url + query, self.key) + p.tags = getattr(p, 'tags', []) + return p + + def find(self, **kwargs): + p = super(ProjectManager, self).find(**kwargs) + p.tags = getattr(p, 'tags', []) + return p def update(self, project, name=None, domain=None, description=None, - enabled=None): + enabled=None, **kwargs): + """Update a project. + + :param project: the project to be updated on the server. + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param str name: the new name of the project. + :param domain: the new domain of the project. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param str description: the new description of the project. + :param bool enabled: whether the project is enabled. + :param kwargs: any other attribute provided will be passed to server. + + :returns: the updated project returned from server. + :rtype: :class:`keystoneclient.v3.projects.Project` + + """ return super(ProjectManager, self).update( project_id=base.getid(project), domain_id=base.getid(domain), name=name, description=description, - enabled=enabled) + enabled=enabled, + **kwargs) def delete(self, project): + """Delete a project. + + :param project: the project to be deleted on the server. + :type project: str or :class:`keystoneclient.v3.projects.Project` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ return super(ProjectManager, self).delete( project_id=base.getid(project)) + + def add_tag(self, project, tag): + """Add a tag to a project. + + :param project: project to add a tag to. + :param tag: str name of tag. + + """ + url = "/projects/%s/tags/%s" % (base.getid(project), + urllib.parse.quote(tag)) + return self._put(url) + + def update_tags(self, project, tags): + """Update tag list of a project. + + Replaces current tag list with list specified in tags parameter. + + :param project: project to update. + :param tags: list of str tag names to add to the project + + :returns: list of tags + + """ + url = "/projects/%s/tags" % base.getid(project) + for tag in tags: + tag = urllib.parse.quote(tag) + resp, body = self.client.put(url, body={"tags": tags}) + return self._prepare_return_value(resp, body['tags']) + + def delete_tag(self, project, tag): + """Remove tag from project. + + :param projectd: project to remove tag from. + :param tag: str name of tag to remove from project + + """ + return self._delete( + "/projects/%s/tags/%s" % (base.getid(project), + urllib.parse.quote(tag))) + + def list_tags(self, project): + """List tags associated with project. + + :param project: project to list tags for. + + :returns: list of str tag names + + """ + url = "/projects/%s/tags" % base.getid(project) + resp, body = self.client.get(url) + return self._prepare_return_value(resp, body['tags']) + + def check_tag(self, project, tag): + """Check if tag is associated with project. + + :param project: project to check tags for. + :param tag: str name of tag + + :returns: true if tag is associated, false otherwise + + """ + url = "/projects/%s/tags/%s" % (base.getid(project), + urllib.parse.quote(tag)) + try: + resp, body = self.client.head(url) + # no errors means found the tag + return self._prepare_return_value(resp, True) + except exceptions.HttpError as ex: + # return false with request_id if include_metadata=True + return self._prepare_return_value(ex.response, False) diff --git a/keystoneclient/v3/regions.py b/keystoneclient/v3/regions.py new file mode 100644 index 000000000..0538a6656 --- /dev/null +++ b/keystoneclient/v3/regions.py @@ -0,0 +1,130 @@ +# 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 debtcollector import removals +from keystoneclient import base + + +class Region(base.Resource): + """Represents a Catalog region. + + Attributes: + * id: a string that identifies the region. + * description: a string that describes the region. + * parent_region_id: a pre-existing region in the backend or its ID + field. Allows for hierarchical region organization. + * enabled: determines whether the endpoint appears in the catalog. + """ + + pass + + +class RegionManager(base.CrudManager): + """Manager class for manipulating Identity regions.""" + + resource_class = Region + collection_key = 'regions' + key = 'region' + + @removals.removed_kwarg( + 'enabled', + message='The enabled parameter is deprecated.', + version='3.18.0', + removal_version='4.0.0') + def create(self, id=None, description=None, enabled=True, + parent_region=None, **kwargs): + """Create a region. + + :param str id: the unique identifier of the region. If not specified an + ID will be created by the server. + :param str description: the description of the region. + :param bool enabled: whether the region is enabled or not, determining + if it appears in the catalog. + :param parent_region: the parent of the region in the hierarchy. + :type parent_region: str or :class:`keystoneclient.v3.regions.Region` + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the created region returned from server. + :rtype: :class:`keystoneclient.v3.regions.Region` + + """ + return super(RegionManager, self).create( + id=id, description=description, enabled=enabled, + parent_region_id=base.getid(parent_region), **kwargs) + + def get(self, region): + """Retrieve a region. + + :param region: the region to be retrieved from the server. + :type region: str or :class:`keystoneclient.v3.regions.Region` + + :returns: the specified region returned from server. + :rtype: :class:`keystoneclient.v3.regions.Region` + + """ + return super(RegionManager, self).get( + region_id=base.getid(region)) + + def list(self, **kwargs): + """List regions. + + :param kwargs: any attributes provided will filter regions on. + + :returns: a list of regions. + :rtype: list of :class:`keystoneclient.v3.regions.Region`. + + """ + return super(RegionManager, self).list( + **kwargs) + + @removals.removed_kwarg( + 'enabled', + message='The enabled parameter is deprecated.', + version='3.18.0', + removal_version='4.0.0') + def update(self, region, description=None, enabled=None, + parent_region=None, **kwargs): + """Update a region. + + :param region: the region to be updated on the server. + :type region: str or :class:`keystoneclient.v3.regions.Region` + :param str description: the new description of the region. + :param bool enabled: determining if the region appears in the catalog + by enabling or disabling it. + :param parent_region: the new parent of the region in the hierarchy. + :type parent_region: str or :class:`keystoneclient.v3.regions.Region` + :param kwargs: any other attribute provided will be passed to server. + + :returns: the updated region returned from server. + :rtype: :class:`keystoneclient.v3.regions.Region` + + """ + return super(RegionManager, self).update( + region_id=base.getid(region), + description=description, + enabled=enabled, + parent_region_id=base.getid(parent_region), + **kwargs) + + def delete(self, region): + """Delete a region. + + :param region: the region to be deleted on the server. + :type region: str or :class:`keystoneclient.v3.regions.Region` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ + return super(RegionManager, self).delete( + region_id=base.getid(region)) diff --git a/keystoneclient/v3/registered_limits.py b/keystoneclient/v3/registered_limits.py new file mode 100644 index 000000000..088aadc9e --- /dev/null +++ b/keystoneclient/v3/registered_limits.py @@ -0,0 +1,160 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class RegisteredLimit(base.Resource): + """Represents a registered limit. + + Attributes: + * id: a UUID that identifies the registered limit + * service_id: a UUID that identifies the service for the limit + * region_id: a UUID that identifies the region for the limit + * resource_name: the name of the resource to limit + * default_limit: the default limit for projects to assume + * description: a description of the registered limit + + """ + + pass + + +class RegisteredLimitManager(base.CrudManager): + """Manager class for registered limits.""" + + resource_class = RegisteredLimit + collection_key = 'registered_limits' + key = 'registered_limit' + + def create(self, service, resource_name, default_limit, + description=None, region=None, **kwargs): + """Create a registered limit. + + :param service: a UUID that identifies the service for the limit. + :type service: str + :param resource_name: the name of the resource to limit. + :type resource_name: str + :param default_limit: the default limit for projects to assume. + :type default_limit: int + :param description: a string that describes the limit + :type description: str + :param region: a UUID that identifies the region for the limit. + :type region: str + + :returns: a reference of the created registered limit. + :rtype: :class:`keystoneclient.v3.registered_limits.RegisteredLimit` + + """ + # NOTE(lbragstad): Keystone's registered limit API supports creation of + # limits in batches. This client accepts a single limit and passes it + # to the identity service as a list of a single item. + limit_data = base.filter_none( + service_id=base.getid(service), + resource_name=resource_name, + default_limit=default_limit, + description=description, + region_id=base.getid(region), + **kwargs + ) + body = {self.collection_key: [limit_data]} + resp, body = self.client.post('/registered_limits', body=body) + registered_limit = body[self.collection_key].pop() + return self._prepare_return_value(resp, + self.resource_class( + self, registered_limit)) + + def update(self, registered_limit, service=None, resource_name=None, + default_limit=None, description=None, region=None, **kwargs): + """Update a registered limit. + + :param registered_limit: + the UUID or reference of the registered limit to update. + :param registered_limit: + str or :class:`keystoneclient.v3.registered_limits.RegisteredLimit` + :param service: a UUID that identifies the service for the limit. + :type service: str + :param resource_name: the name of the resource to limit. + :type resource_name: str + :param default_limit: the default limit for projects to assume. + :type default_limit: int + :param description: a string that describes the limit + :type description: str + :param region: a UUID that identifies the region for the limit. + :type region: str + + :returns: a reference of the updated registered limit. + :rtype: :class:`keystoneclient.v3.registered_limits.RegisteredLimit` + + """ + return super(RegisteredLimitManager, self).update( + registered_limit_id=base.getid(registered_limit), + service_id=base.getid(service), + resource_name=resource_name, + default_limit=default_limit, + description=description, + region=region, + **kwargs + ) + + def get(self, registered_limit): + """Retrieve a registered limit. + + :param registered_limit: the registered limit to get. + :type registered_limit: + str or :class:`keystoneclient.v3.registered_limits.RegisteredLimit` + + :returns: a specific registered limit. + :rtype: :class:`keystoneclient.v3.registered_limits.RegisteredLimit` + + """ + return super(RegisteredLimitManager, self).get( + registered_limit_id=base.getid(registered_limit)) + + def list(self, service=None, resource_name=None, region=None, **kwargs): + """List registered limits. + + Any parameter provided will be passed to the server as a filter. + + :param service: filter registered limits by service + :type service: a UUID or :class:`keystoneclient.v3.services.Service` + :param resource_name: filter registered limits by resource name + :type resource_name: str + :param region: filter registered limits by region + :type region: a UUID or :class:`keystoneclient.v3.regions.Region` + + :returns: a list of registered limits. + :rtype: list of + :class:`keystoneclient.v3.registered_limits.RegisteredLimit` + + """ + return super(RegisteredLimitManager, self).list( + service_id=base.getid(service), + resource_name=resource_name, + region_id=base.getid(region), + **kwargs) + + def delete(self, registered_limit): + """Delete a registered limit. + + :param registered_limit: the registered limit to delete. + :type registered_limit: + str or :class:`keystoneclient.v3.registered_limits.RegisteredLimit` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ + registered_limit_id = base.getid(registered_limit) + return super(RegisteredLimitManager, self).delete( + registered_limit_id=registered_limit_id + ) diff --git a/keystoneclient/v3/role_assignments.py b/keystoneclient/v3/role_assignments.py new file mode 100644 index 000000000..ce1e550a0 --- /dev/null +++ b/keystoneclient/v3/role_assignments.py @@ -0,0 +1,148 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ + + +class RoleAssignment(base.Resource): + """Represents an Identity role assignment. + + Attributes: + * role: an object which contains a role uuid + * user or group: an object which contains either a user or + group uuid + * scope: an object which has either a project or domain object + containing an uuid + """ + + pass + + +class RoleAssignmentManager(base.CrudManager): + """Manager class for manipulating Identity roles assignments.""" + + resource_class = RoleAssignment + collection_key = 'role_assignments' + key = 'role_assignment' + + def _check_not_user_and_group(self, user, group): + if user and group: + msg = _('Specify either a user or group, not both') + raise exceptions.ValidationError(msg) + + def _check_not_domain_and_project(self, domain, project): + if domain and project: + msg = _('Specify either a domain or project, not both') + raise exceptions.ValidationError(msg) + + def _check_not_system_and_domain(self, system, domain): + if system and domain: + msg = _('Specify either system or domain, not both') + raise exceptions.ValidationError(msg) + + def _check_not_system_and_project(self, system, project): + if system and project: + msg = _('Specify either system or project, not both') + raise exceptions.ValidationError(msg) + + def _check_system_value(self, system): + if system and system != 'all': + msg = _("Only a system scope of 'all' is currently supported") + raise exceptions.ValidationError(msg) + + def list(self, user=None, group=None, project=None, domain=None, + system=False, role=None, effective=False, + os_inherit_extension_inherited_to=None, include_subtree=False, + include_names=False): + """List role assignments. + + If no arguments are provided, all role assignments in the + system will be listed. + + If both user and group are provided, a ValidationError will be + raised. If both domain and project are provided, it will also + raise a ValidationError. + + :param user: User to be used as query filter. (optional) + :param group: Group to be used as query filter. (optional) + :param project: Project to be used as query filter. + (optional) + :param domain: Domain to be used as query + filter. (optional) + :param system: Boolean to be used to filter system assignments. + (optional) + :param role: Role to be used as query filter. (optional) + :param boolean effective: return effective role + assignments. (optional) + :param string os_inherit_extension_inherited_to: + return inherited role assignments for either 'projects' or + 'domains'. (optional) + :param boolean include_subtree: Include subtree (optional) + :param boolean include_names: Display names instead + of IDs. (optional) + """ + self._check_not_user_and_group(user, group) + self._check_not_domain_and_project(domain, project) + self._check_not_system_and_domain(system, domain) + self._check_not_system_and_project(system, project) + self._check_system_value(system) + + query_params = {} + if user: + query_params['user.id'] = base.getid(user) + if group: + query_params['group.id'] = base.getid(group) + if project: + query_params['scope.project.id'] = base.getid(project) + if domain: + query_params['scope.domain.id'] = base.getid(domain) + if system: + query_params['scope.system'] = system + if role: + query_params['role.id'] = base.getid(role) + if effective: + query_params['effective'] = effective + if include_names: + query_params['include_names'] = include_names + if os_inherit_extension_inherited_to: + query_params['scope.OS-INHERIT:inherited_to'] = ( + os_inherit_extension_inherited_to) + if include_subtree: + query_params['include_subtree'] = include_subtree + + return super(RoleAssignmentManager, self).list(**query_params) + + def create(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('Create not supported for role assignments')) + + def update(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('Update not supported for role assignments')) + + def get(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('Get not supported for role assignments')) + + def find(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('Find not supported for role assignments')) + + def put(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('Put not supported for role assignments')) + + def delete(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('Delete not supported for role assignments')) diff --git a/keystoneclient/v3/roles.py b/keystoneclient/v3/roles.py index a93042666..3364bd88b 100644 --- a/keystoneclient/v3/roles.py +++ b/keystoneclient/v3/roles.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -14,8 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. +from debtcollector import removals + from keystoneclient import base from keystoneclient import exceptions +from keystoneclient.i18n import _ class Role(base.Resource): @@ -24,18 +27,35 @@ class Role(base.Resource): Attributes: * id: a uuid that identifies the role * name: user-facing identifier + * domain: optional domain for the role + + """ + + pass + + +class InferenceRule(base.Resource): + """Represents a rule that states one role implies another. + + Attributes: + * prior_role: this role implies the other + * implied_role: this role is implied by the other """ + pass class RoleManager(base.CrudManager): """Manager class for manipulating Identity roles.""" + resource_class = Role collection_key = 'roles' key = 'role' + deprecation_msg = 'keystoneclient.v3.roles.InferenceRuleManager' - def _role_grants_base_url(self, user, group, domain, project): + def _role_grants_base_url(self, user, group, system, domain, project, + use_inherit_extension): # When called, we have already checked that only one of user & group # and one of domain & project have been specified params = {} @@ -46,6 +66,21 @@ def _role_grants_base_url(self, user, group, domain, project): elif domain: params['domain_id'] = base.getid(domain) base_url = '/domains/%(domain_id)s' + elif system: + if system == 'all': + base_url = '/system' + else: + # NOTE(lbragstad): If we've made it this far, a user is + # attempting to do something with system scope that isn't + # supported yet (e.g. 'all' is currently the only supported + # system scope). In the future that may change but until then + # we should fail like we would if a user provided a bogus + # project name or domain ID. + msg = _("Only a system scope of 'all' is currently supported") + raise exceptions.ValidationError(msg) + + if use_inherit_extension: + base_url = '/OS-INHERIT' + base_url if user: params['user_id'] = base.getid(user) @@ -56,75 +91,481 @@ def _role_grants_base_url(self, user, group, domain, project): return base_url % params - def _require_domain_xor_project(self, domain, project): - if (domain and project) or (not domain and not project): - msg = 'Specify either a domain or project, not both' - raise exceptions.ValidationError(msg) + def _enforce_mutually_exclusive_group(self, system, domain, project): + if not system: + if domain and project: + msg = _('Specify either a domain or project, not both') + raise exceptions.ValidationError(msg) + elif not (domain or project): + msg = _('Must specify either system, domain, or project') + raise exceptions.ValidationError(msg) + elif system: + if domain and project: + msg = _( + 'Specify either system, domain, or project, not all three.' + ) + raise exceptions.ValidationError(msg) + if domain: + msg = _('Specify either system or a domain, not both') + raise exceptions.ValidationError(msg) + if project: + msg = _('Specify either a system or project, not both') + raise exceptions.ValidationError(msg) def _require_user_xor_group(self, user, group): - if (user and group) or (not user and not group): - msg = 'Specify either a user or group, not both' + if user and group: + msg = _('Specify either a user or group, not both') + raise exceptions.ValidationError(msg) + elif not (user or group): + msg = _('Must specify either a user or group') raise exceptions.ValidationError(msg) - def create(self, name): + def create(self, name, domain=None, **kwargs): + """Create a role. + + :param str name: the name of the role. + :param domain: the domain of the role. If a value is passed it is a + domain-scoped role, otherwise it's a global role. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the created role returned from server. + :rtype: :class:`keystoneclient.v3.roles.Role` + + """ + domain_id = None + if domain: + domain_id = base.getid(domain) + return super(RoleManager, self).create( - name=name) + name=name, + domain_id=domain_id, + **kwargs) def get(self, role): - return super(RoleManager, self).get( - role_id=base.getid(role)) + """Retrieve a role. - def list(self, user=None, group=None, domain=None, project=None): - """Lists roles and role grants. + :param role: the role to be retrieved from the server. + :type role: str or :class:`keystoneclient.v3.roles.Role` - If no arguments are provided, all roles in the system will be listed. + :returns: the specified role returned from server. + :rtype: :class:`keystoneclient.v3.roles.Role` - If a user or group is specified, you must also specify either a - domain or project to list role grants on that pair. """ + return super(RoleManager, self).get(role_id=base.getid(role)) + + def list(self, user=None, group=None, system=None, domain=None, + project=None, os_inherit_extension_inherited=False, **kwargs): + """List roles and role grants. + :param user: filter in role grants for the specified user on a + resource. Domain or project must be specified. + User and group are mutually exclusive. + :type user: str or :class:`keystoneclient.v3.users.User` + :param group: filter in role grants for the specified group on a + resource. Domain or project must be specified. + User and group are mutually exclusive. + :type group: str or :class:`keystoneclient.v3.groups.Group` + :param domain: filter in role grants on the specified domain. Either + user or group must be specified. Project, domain, and + system are mutually exclusive. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param project: filter in role grants on the specified project. Either + user or group must be specified. Project, domain and + system are mutually exclusive. + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param bool os_inherit_extension_inherited: OS-INHERIT will be used. + It provides the ability for + projects to inherit role + assignments from their + domains or from parent + projects in the hierarchy. + :param kwargs: any other attribute provided will filter roles on. + + :returns: a list of roles. + :rtype: list of :class:`keystoneclient.v3.roles.Role` + + """ + if os_inherit_extension_inherited: + kwargs['tail'] = '/inherited_to_projects' if user or group: self._require_user_xor_group(user, group) - self._require_domain_xor_project(domain, project) + self._enforce_mutually_exclusive_group(system, domain, project) + + base_url = self._role_grants_base_url( + user, group, system, domain, project, + os_inherit_extension_inherited + ) + return super(RoleManager, self).list(base_url=base_url, + **kwargs) - return super(RoleManager, self).list( - base_url=self._role_grants_base_url(user, group, - domain, project)) + return super(RoleManager, self).list(**kwargs) - return super(RoleManager, self).list() + def update(self, role, name=None, **kwargs): + """Update a role. - def update(self, role, name=None): + :param role: the role to be updated on the server. + :type role: str or :class:`keystoneclient.v3.roles.Role` + :param str name: the new name of the role. + :param kwargs: any other attribute provided will be passed to server. + + :returns: the updated role returned from server. + :rtype: :class:`keystoneclient.v3.roles.Role` + + """ return super(RoleManager, self).update( role_id=base.getid(role), - name=name) + name=name, + **kwargs) def delete(self, role): + """Delete a role. + + When a role is deleted all the role inferences that have deleted role + as prior role will be deleted as well. + + :param role: the role to be deleted on the server. + :type role: str or :class:`keystoneclient.v3.roles.Role` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ return super(RoleManager, self).delete( role_id=base.getid(role)) - def grant(self, role, user=None, group=None, domain=None, project=None): - """Grants a role to a user or group on a domain or project.""" - self._require_domain_xor_project(domain, project) + def grant(self, role, user=None, group=None, system=None, domain=None, + project=None, os_inherit_extension_inherited=False, **kwargs): + """Grant a role to a user or group on a domain or project. + + :param role: the role to be granted on the server. + :type role: str or :class:`keystoneclient.v3.roles.Role` + :param user: the specified user to have the role granted on a resource. + Domain or project must be specified. User and group are + mutually exclusive. + :type user: str or :class:`keystoneclient.v3.users.User` + :param group: the specified group to have the role granted on a + resource. Domain or project must be specified. + User and group are mutually exclusive. + :type group: str or :class:`keystoneclient.v3.groups.Group` + :param system: system information to grant the role on. Project, + domain, and system are mutually exclusive. + :type system: str + :param domain: the domain in which the role will be granted. Either + user or group must be specified. Project, domain, and + system are mutually exclusive. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param project: the project in which the role will be granted. Either + user or group must be specified. Project, domain, and + system are mutually exclusive. + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param bool os_inherit_extension_inherited: OS-INHERIT will be used. + It provides the ability for + projects to inherit role + assignments from their + domains or from parent + projects in the hierarchy. + :param kwargs: any other attribute provided will be passed to server. + + :returns: the granted role returned from server. + :rtype: :class:`keystoneclient.v3.roles.Role` + + """ + self._enforce_mutually_exclusive_group(system, domain, project) self._require_user_xor_group(user, group) - return super(RoleManager, self).put( - base_url=self._role_grants_base_url(user, group, domain, project), - role_id=base.getid(role)) + if os_inherit_extension_inherited: + kwargs['tail'] = '/inherited_to_projects' + + base_url = self._role_grants_base_url( + user, group, system, domain, project, + os_inherit_extension_inherited) + return super(RoleManager, self).put(base_url=base_url, + role_id=base.getid(role), + **kwargs) + + def check(self, role, user=None, group=None, system=None, domain=None, + project=None, os_inherit_extension_inherited=False, **kwargs): + """Check if a user or group has a role on a domain or project. + + :param user: check for role grants for the specified user on a + resource. Domain or project must be specified. + User and group are mutually exclusive. + :type user: str or :class:`keystoneclient.v3.users.User` + :param group: check for role grants for the specified group on a + resource. Domain or project must be specified. + User and group are mutually exclusive. + :type group: str or :class:`keystoneclient.v3.groups.Group` + :param system: check for role grants on the system. Project, domain, + and system are mutually exclusive. + :type system: str + :param domain: check for role grants on the specified domain. Either + user or group must be specified. Project, domain, and + system are mutually exclusive. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param project: check for role grants on the specified project. Either + user or group must be specified. Project, domain, and + system are mutually exclusive. + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param bool os_inherit_extension_inherited: OS-INHERIT will be used. + It provides the ability for + projects to inherit role + assignments from their + domains or from parent + projects in the hierarchy. + :param kwargs: any other attribute provided will be passed to server. + + :returns: the specified role returned from server if it exists. + :rtype: :class:`keystoneclient.v3.roles.Role` - def check(self, role, user=None, group=None, domain=None, project=None): - """Checks if a user or group has a role on a domain or project.""" - self._require_domain_xor_project(domain, project) + :returns: Response object with 204 status if specified role + doesn't exist. + :rtype: :class:`requests.models.Response` + + """ + self._enforce_mutually_exclusive_group(system, domain, project) self._require_user_xor_group(user, group) + if os_inherit_extension_inherited: + kwargs['tail'] = '/inherited_to_projects' + + base_url = self._role_grants_base_url( + user, group, system, domain, project, + os_inherit_extension_inherited) return super(RoleManager, self).head( - base_url=self._role_grants_base_url(user, group, domain, project), - role_id=base.getid(role)) + base_url=base_url, + role_id=base.getid(role), + os_inherit_extension_inherited=os_inherit_extension_inherited, + **kwargs) + + def revoke(self, role, user=None, group=None, system=None, domain=None, + project=None, os_inherit_extension_inherited=False, **kwargs): + """Revoke a role from a user or group on a domain or project. + + :param user: revoke role grants for the specified user on a + resource. Domain or project must be specified. + User and group are mutually exclusive. + :type user: str or :class:`keystoneclient.v3.users.User` + :param group: revoke role grants for the specified group on a + resource. Domain or project must be specified. + User and group are mutually exclusive. + :type group: str or :class:`keystoneclient.v3.groups.Group` + :param system: revoke role grants on the system. Project, domain, and + system are mutually exclusive. + :type system: str + :param domain: revoke role grants on the specified domain. Either + user or group must be specified. Project, domain, and + system are mutually exclusive. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param project: revoke role grants on the specified project. Either + user or group must be specified. Project, domain, and + system are mutually exclusive. + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param bool os_inherit_extension_inherited: OS-INHERIT will be used. + It provides the ability for + projects to inherit role + assignments from their + domains or from parent + projects in the hierarchy. + :param kwargs: any other attribute provided will be passed to server. - def revoke(self, role, user=None, group=None, domain=None, project=None): - """Revokes a role from a user or group on a domain or project.""" - self._require_domain_xor_project(domain, project) + :returns: the revoked role returned from server. + :rtype: list of :class:`keystoneclient.v3.roles.Role` + + """ + self._enforce_mutually_exclusive_group(system, domain, project) self._require_user_xor_group(user, group) + if os_inherit_extension_inherited: + kwargs['tail'] = '/inherited_to_projects' + + base_url = self._role_grants_base_url( + user, group, system, domain, project, + os_inherit_extension_inherited) return super(RoleManager, self).delete( - base_url=self._role_grants_base_url(user, group, domain, project), - role_id=base.getid(role)) + base_url=base_url, + role_id=base.getid(role), + os_inherit_extension_inherited=os_inherit_extension_inherited, + **kwargs) + + @removals.remove(message='Use %s.create instead.' % deprecation_msg, + version='3.9.0', removal_version='4.0.0') + def create_implied(self, prior_role, implied_role, **kwargs): + return InferenceRuleManager(self.client).create(prior_role, + implied_role) + + @removals.remove(message='Use %s.delete instead.' % deprecation_msg, + version='3.9.0', removal_version='4.0.0') + def delete_implied(self, prior_role, implied_role, **kwargs): + return InferenceRuleManager(self.client).delete(prior_role, + implied_role) + + @removals.remove(message='Use %s.get instead.' % deprecation_msg, + version='3.9.0', removal_version='4.0.0') + def get_implied(self, prior_role, implied_role, **kwargs): + return InferenceRuleManager(self.client).get(prior_role, + implied_role) + + @removals.remove(message='Use %s.check instead.' % deprecation_msg, + version='3.9.0', removal_version='4.0.0') + def check_implied(self, prior_role, implied_role, **kwargs): + return InferenceRuleManager(self.client).check(prior_role, + implied_role) + + @removals.remove(message='Use %s.list_inference_roles' % deprecation_msg, + version='3.9.0', removal_version='4.0.0') + def list_role_inferences(self, **kwargs): + return InferenceRuleManager(self.client).list_inference_roles() + + +class InferenceRuleManager(base.CrudManager): + """Manager class for manipulating Identity inference rules.""" + + resource_class = InferenceRule + collection_key = 'role_inferences' + key = 'role_inference' + + def _implied_role_url_tail(self, prior_role, implied_role): + base_url = ('/%(prior_role_id)s/implies/%(implied_role_id)s' % + {'prior_role_id': base.getid(prior_role), + 'implied_role_id': base.getid(implied_role)}) + return base_url + + def create(self, prior_role, implied_role): + """Create an inference rule. + + An inference rule is comprised of two roles, a prior role and an + implied role. The prior role will imply the implied role. + + Valid HTTP return codes: + + * 201: Resource is created successfully + * 404: A role cannot be found + * 409: The inference rule already exists + + :param prior_role: the role which implies ``implied_role``. + :type role: str or :class:`keystoneclient.v3.roles.Role` + :param implied_role: the role which is implied by ``prior_role``. + :type role: str or :class:`keystoneclient.v3.roles.Role` + + :returns: a newly created role inference returned from server. + :rtype: :class:`keystoneclient.v3.roles.InferenceRule` + + """ + url_tail = self._implied_role_url_tail(prior_role, implied_role) + _resp, body = self.client.put("/roles" + url_tail) + return self._prepare_return_value( + _resp, self.resource_class(self, body['role_inference'])) + + def delete(self, prior_role, implied_role): + """Delete an inference rule. + + When deleting an inference rule, both roles are required. Note that + neither role is deleted, only the inference relationship is dissolved. + + Valid HTTP return codes: + + * 204: Delete request is accepted + * 404: A role cannot be found + + :param prior_role: the role which implies ``implied_role``. + :type role: str or :class:`keystoneclient.v3.roles.Role` + :param implied_role: the role which is implied by ``prior_role``. + :type role: str or :class:`keystoneclient.v3.roles.Role` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ + url_tail = self._implied_role_url_tail(prior_role, implied_role) + return self._delete("/roles" + url_tail) + + def get(self, prior_role, implied_role): + """Retrieve an inference rule. + + Valid HTTP return codes: + + * 200: Inference rule is returned + * 404: A role cannot be found + + :param prior_role: the role which implies ``implied_role``. + :type role: str or :class:`keystoneclient.v3.roles.Role` + :param implied_role: the role which is implied by ``prior_role``. + :type role: str or :class:`keystoneclient.v3.roles.Role` + + :returns: the specified role inference returned from server. + :rtype: :class:`keystoneclient.v3.roles.InferenceRule` + + """ + url_tail = self._implied_role_url_tail(prior_role, implied_role) + _resp, body = self.client.get("/roles" + url_tail) + return self._prepare_return_value( + _resp, self.resource_class(self, body['role_inference'])) + + def list(self, prior_role): + """List all roles that a role may imply. + + Valid HTTP return codes: + + * 200: List of inference rules are returned + * 404: A role cannot be found + + :param prior_role: the role which implies ``implied_role``. + :type role: str or :class:`keystoneclient.v3.roles.Role` + + :returns: the specified role inference returned from server. + :rtype: :class:`keystoneclient.v3.roles.InferenceRule` + + """ + url_tail = ('/%s/implies' % base.getid(prior_role)) + _resp, body = self.client.get("/roles" + url_tail) + return self._prepare_return_value( + _resp, self.resource_class(self, body['role_inference'])) + + def check(self, prior_role, implied_role): + """Check if an inference rule exists. + + Valid HTTP return codes: + + * 204: The rule inference exists + * 404: A role cannot be found + + :param prior_role: the role which implies ``implied_role``. + :type role: str or :class:`keystoneclient.v3.roles.Role` + :param implied_role: the role which is implied by ``prior_role``. + :type role: str or :class:`keystoneclient.v3.roles.Role` + + :returns: response object with 204 status returned from server. + :rtype: :class:`requests.models.Response` + + """ + url_tail = self._implied_role_url_tail(prior_role, implied_role) + return self._head("/roles" + url_tail) + + def list_inference_roles(self): + """List all rule inferences. + + Valid HTTP return codes: + + * 200: All inference rules are returned + + :param kwargs: attributes provided will be passed to the server. + + :returns: a list of inference rules. + :rtype: list of :class:`keystoneclient.v3.roles.InferenceRule` + + """ + return super(InferenceRuleManager, self).list() + + def update(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('Update not supported for rule inferences')) + + def find(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('Find not supported for rule inferences')) + + def put(self, **kwargs): + raise exceptions.MethodNotImplemented( + _('Put not supported for rule inferences')) diff --git a/keystoneclient/v3/services.py b/keystoneclient/v3/services.py index d2134d43d..631940e80 100644 --- a/keystoneclient/v3/services.py +++ b/keystoneclient/v3/services.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -22,39 +22,114 @@ class Service(base.Resource): Attributes: * id: a uuid that identifies the service - * name: user-facing name of the service (e.g. Keystone) - * type: 'compute', 'identity', etc + * name: the user-facing name of the service (e.g. Keystone) + * description: a description of the service + * type: the type of the service (e.g. 'compute', 'identity') * enabled: determines whether the service appears in the catalog """ + pass class ServiceManager(base.CrudManager): """Manager class for manipulating Identity services.""" + resource_class = Service collection_key = 'services' key = 'service' - def create(self, name, type, enabled=True, **kwargs): + def create(self, name, type=None, + enabled=True, description=None, **kwargs): + """Create a service. + + :param str name: the name of the service. + :param str type: the type of the service. + :param bool enabled: whether the service appears in the catalog. + :param str description: the description of the service. + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the created service returned from server. + :rtype: :class:`keystoneclient.v3.services.Service` + + """ + type_arg = type or kwargs.pop('service_type', None) return super(ServiceManager, self).create( name=name, - type=type, + type=type_arg, + description=description, enabled=enabled, **kwargs) def get(self, service): + """Retrieve a service. + + :param service: the service to be retrieved from the server. + :type service: str or :class:`keystoneclient.v3.services.Service` + + :returns: the specified service returned from server. + :rtype: :class:`keystoneclient.v3.services.Service` + + """ return super(ServiceManager, self).get( service_id=base.getid(service)) - def update(self, service, name=None, type=None, enabled=None, **kwargs): + def list(self, name=None, type=None, **kwargs): + """List services. + + :param str name: the name of the services to be filtered on. + :param str type: the type of the services to be filtered on. + :param kwargs: any other attribute provided will filter services on. + + :returns: a list of services. + :rtype: list of :class:`keystoneclient.v3.services.Service` + + """ + type_arg = type or kwargs.pop('service_type', None) + return super(ServiceManager, self).list( + name=name, + type=type_arg, + **kwargs) + + def update(self, service, name=None, type=None, enabled=None, + description=None, **kwargs): + """Update a service. + + :param service: the service to be updated on the server. + :type service: str or :class:`keystoneclient.v3.services.Service` + :param str name: the new name of the service. + :param str type: the new type of the service. + :param bool enabled: whether the service appears in the catalog. + :param str description: the new description of the service. + :param kwargs: any other attribute provided will be passed to server. + + :returns: the updated service returned from server. + :rtype: :class:`keystoneclient.v3.services.Service` + + """ + type_arg = type or kwargs.pop('service_type', None) return super(ServiceManager, self).update( service_id=base.getid(service), name=name, - type=type, + type=type_arg, + description=description, enabled=enabled, **kwargs) - def delete(self, service): + def delete(self, service=None, id=None): + """Delete a service. + + :param service: the service to be deleted on the server. + :type service: str or :class:`keystoneclient.v3.services.Service` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ + if service: + service_id = base.getid(service) + else: + service_id = id return super(ServiceManager, self).delete( - service_id=base.getid(service)) + service_id=service_id) diff --git a/keystoneclient/v3/system.py b/keystoneclient/v3/system.py new file mode 100644 index 000000000..8d3edafdd --- /dev/null +++ b/keystoneclient/v3/system.py @@ -0,0 +1,26 @@ +# Copyright 2021 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. + +from keystoneclient import base + + +class System(base.Resource): + """Represents the deployment system, with all the services in it. + + Attributes: + * all: boolean + """ + + pass diff --git a/keystoneclient/v3/tokens.py b/keystoneclient/v3/tokens.py new file mode 100644 index 000000000..7e0cb07a8 --- /dev/null +++ b/keystoneclient/v3/tokens.py @@ -0,0 +1,121 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import access +from keystoneclient import base + + +def _calc_id(token): + if isinstance(token, access.AccessInfo): + return token.auth_token + + return base.getid(token) + + +class TokenManager(object): + """Manager class for manipulating Identity tokens.""" + + def __init__(self, client): + self._client = client + + def revoke_token(self, token): + """Revoke a token. + + :param token: The token to be revoked. + :type token: str or :class:`keystoneclient.access.AccessInfo` + + """ + token_id = _calc_id(token) + headers = {'X-Subject-Token': token_id} + return self._client.delete('/auth/tokens', headers=headers) + + def get_revoked(self, audit_id_only=False): + """Get revoked tokens list. + + :param bool audit_id_only: If true, the server is requested to not send + token IDs, but only audit IDs instead. + **New in version 2.2.0.** + :returns: A dict containing ``signed`` which is a CMS formatted string + if the server signed the response. If `audit_id_only` is true + then the response may be a dict containing ``revoked`` which + is the list of token audit IDs and expiration times. + :rtype: dict + + """ + path = '/auth/tokens/OS-PKI/revoked' + if audit_id_only: + path += '?audit_id_only' + resp, body = self._client.get(path) + return body + + def get_token_data(self, token, include_catalog=True, allow_expired=False, + access_rules_support=None): + """Fetch the data about a token from the identity server. + + :param str token: The ID of the token to be fetched. + :param bool include_catalog: Whether the service catalog should be + included in the response. + :param allow_expired: If True the token will be validated and returned + if it has already expired. + :param access_rules_support: Version number indicating that the client + is capable of enforcing keystone + access rules, if unset this client + does not support access rules. + :type access_rules_support: float + + :rtype: dict + + """ + headers = {'X-Subject-Token': token} + if access_rules_support: + headers['OpenStack-Identity-Access-Rules'] = access_rules_support + flags = [] + + url = '/auth/tokens' + + if not include_catalog: + flags.append('nocatalog') + if allow_expired: + flags.append('allow_expired=1') + + if flags: + url = '%s?%s' % (url, '&'.join(flags)) + + resp, body = self._client.get(url, headers=headers) + return body + + def validate(self, token, include_catalog=True, allow_expired=False, + access_rules_support=None): + """Validate a token. + + :param token: The token to be validated. + :type token: str or :class:`keystoneclient.access.AccessInfo` + :param include_catalog: If False, the response is requested to not + include the catalog. + :param allow_expired: If True the token will be validated and returned + if it has already expired. + :type allow_expired: bool + :param access_rules_support: Version number indicating that the client + is capable of enforcing keystone + access rules, if unset this client + does not support access rules. + :type access_rules_support: float + + :rtype: :class:`keystoneclient.access.AccessInfoV3` + + """ + token_id = _calc_id(token) + body = self.get_token_data(token_id, + include_catalog=include_catalog, + allow_expired=allow_expired, + access_rules_support=access_rules_support) + return access.AccessInfo.factory(auth_token=token_id, body=body) diff --git a/keystoneclient/v3/users.py b/keystoneclient/v3/users.py index 363cef338..90cd51c45 100644 --- a/keystoneclient/v3/users.py +++ b/keystoneclient/v3/users.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Nebula, Inc. # All Rights Reserved. # @@ -14,7 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. +from debtcollector import renames + from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.i18n import _ class User(base.Resource): @@ -24,32 +28,102 @@ class User(base.Resource): * id: a uuid that identifies the user """ + pass class UserManager(base.CrudManager): """Manager class for manipulating Identity users.""" + resource_class = User collection_key = 'users' key = 'user' def _require_user_and_group(self, user, group): if not (user and group): - msg = 'Specify both a user and a group' + msg = _('Specify both a user and a group') raise exceptions.ValidationError(msg) + @renames.renamed_kwarg('project', 'default_project', version='1.7.0', + removal_version='2.0.0') def create(self, name, domain=None, project=None, password=None, - email=None, description=None, enabled=True): - return super(UserManager, self).create( - name=name, - domain_id=base.getid(domain), - project_id=base.getid(project), - password=password, - email=email, - description=description, - enabled=enabled) + email=None, description=None, enabled=True, + default_project=None, **kwargs): + """Create a user. + + :param str name: the name of the user. + :param domain: the domain of the user. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param project: the default project of the user. + (deprecated, see warning below) + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param str password: the password for the user. + :param str email: the email address of the user. + :param str description: a description of the user. + :param bool enabled: whether the user is enabled. + :param default_project: the default project of the user. + :type default_project: str or + :class:`keystoneclient.v3.projects.Project` + :param kwargs: any other attribute provided will be passed to the + server. + + :returns: the created user returned from server. + :rtype: :class:`keystoneclient.v3.users.User` + + .. warning:: + + The project argument is deprecated as of the 1.7.0 release in favor + of default_project and may be removed in the 2.0.0 release. + + If both default_project and project is provided, the default_project + will be used. + + """ + default_project_id = base.getid(default_project) or base.getid(project) + user_data = base.filter_none(name=name, + domain_id=base.getid(domain), + default_project_id=default_project_id, + password=password, + email=email, + description=description, + enabled=enabled, + **kwargs) + + return self._post('/users', {'user': user_data}, 'user', + log=not bool(password)) + + @renames.renamed_kwarg('project', 'default_project', version='1.7.0', + removal_version='2.0.0') + def list(self, project=None, domain=None, group=None, default_project=None, + **kwargs): + """List users. + + :param project: the default project of the users to be filtered on. + (deprecated, see warning below) + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param domain: the domain of the users to be filtered on. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param group: the group in which the users are member of. + :type group: str or :class:`keystoneclient.v3.groups.Group` + :param default_project: the default project of the users to be filtered + on. + :type default_project: str or + :class:`keystoneclient.v3.projects.Project` + :param kwargs: any other attribute provided will filter users on. - def list(self, project=None, domain=None, group=None): + :returns: a list of users. + :rtype: list of :class:`keystoneclient.v3.users.User`. + + .. warning:: + + The project argument is deprecated as of the 1.7.0 release in favor + of default_project and may be removed in the 2.0.0 release. + + If both default_project and project is provided, the default_project + will be used. + + """ + default_project_id = base.getid(default_project) or base.getid(project) if group: base_url = '/groups/%s' % base.getid(group) else: @@ -58,25 +132,111 @@ def list(self, project=None, domain=None, group=None): return super(UserManager, self).list( base_url=base_url, domain_id=base.getid(domain), - project_id=base.getid(project)) + default_project_id=default_project_id, + **kwargs) def get(self, user): + """Retrieve a user. + + :param user: the user to be retrieved from the server. + :type user: str or :class:`keystoneclient.v3.users.User` + + :returns: the specified user returned from server. + :rtype: :class:`keystoneclient.v3.users.User` + + """ return super(UserManager, self).get( user_id=base.getid(user)) + @renames.renamed_kwarg('project', 'default_project', version='1.7.0', + removal_version='2.0.0') def update(self, user, name=None, domain=None, project=None, password=None, - email=None, description=None, enabled=None): - return super(UserManager, self).update( - user_id=base.getid(user), - name=name, - domain_id=base.getid(domain), - project_id=base.getid(project), - password=password, - email=email, - description=description, - enabled=enabled) + email=None, description=None, enabled=None, + default_project=None, **kwargs): + """Update a user. + + :param user: the user to be updated on the server. + :type user: str or :class:`keystoneclient.v3.users.User` + :param str name: the new name of the user. + :param domain: the new domain of the user. + :type domain: str or :class:`keystoneclient.v3.domains.Domain` + :param project: the new default project of the user. + (deprecated, see warning below) + :type project: str or :class:`keystoneclient.v3.projects.Project` + :param str password: the new password of the user. + :param str email: the new email of the user. + :param str description: the newdescription of the user. + :param bool enabled: whether the user is enabled. + :param default_project: the new default project of the user. + :type default_project: str or + :class:`keystoneclient.v3.projects.Project` + :param kwargs: any other attribute provided will be passed to server. + + :returns: the updated user returned from server. + :rtype: :class:`keystoneclient.v3.users.User` + + .. warning:: + + The project argument is deprecated as of the 1.7.0 release in favor + of default_project and may be removed in the 2.0.0 release. + + If both default_project and project is provided, the default_project + will be used. + + """ + default_project_id = base.getid(default_project) or base.getid(project) + user_data = base.filter_none(name=name, + domain_id=base.getid(domain), + default_project_id=default_project_id, + password=password, + email=email, + description=description, + enabled=enabled, + **kwargs) + + return self._update('/users/%s' % base.getid(user), + {'user': user_data}, + 'user', + method='PATCH', + log=False) + + def update_password(self, old_password, new_password): + """Update the password for the user the token belongs to. + + :param str old_password: the user's old password + :param str new_password: the user's new password + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ + if not (old_password and new_password): + msg = _('Specify both the current password and a new password') + raise exceptions.ValidationError(msg) + + if old_password == new_password: + msg = _('Old password and new password must be different.') + raise exceptions.ValidationError(msg) + + params = {'user': {'password': new_password, + 'original_password': old_password}} + + base_url = '/users/%s/password' % self.client.user_id + + return self._update(base_url, params, method='POST', log=False) def add_to_group(self, user, group): + """Add the specified user as a member of the specified group. + + :param user: the user to be added to the group. + :type user: str or :class:`keystoneclient.v3.users.User` + :param group: the group to put the user in. + :type group: str or :class:`keystoneclient.v3.groups.Group` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ self._require_user_and_group(user, group) base_url = '/groups/%s' % base.getid(group) @@ -85,6 +245,17 @@ def add_to_group(self, user, group): user_id=base.getid(user)) def check_in_group(self, user, group): + """Check if the specified user is a member of the specified group. + + :param user: the user to be verified in the group. + :type user: str or :class:`keystoneclient.v3.users.User` + :param group: the group to check the user in. + :type group: str or :class:`keystoneclient.v3.groups.Group` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ self._require_user_and_group(user, group) base_url = '/groups/%s' % base.getid(group) @@ -93,6 +264,17 @@ def check_in_group(self, user, group): user_id=base.getid(user)) def remove_from_group(self, user, group): + """Remove the specified user from the specified group. + + :param user: the user to be removed from the group. + :type user: str or :class:`keystoneclient.v3.users.User` + :param group: the group to remove the user from. + :type group: str or :class:`keystoneclient.v3.groups.Group` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ self._require_user_and_group(user, group) base_url = '/groups/%s' % base.getid(group) @@ -101,5 +283,14 @@ def remove_from_group(self, user, group): user_id=base.getid(user)) def delete(self, user): + """Delete a user. + + :param user: the user to be deleted on the server. + :type user: str or :class:`keystoneclient.v3.users.User` + + :returns: Response object with 204 status. + :rtype: :class:`requests.models.Response` + + """ return super(UserManager, self).delete( user_id=base.getid(user)) diff --git a/openstack-common.conf b/openstack-common.conf deleted file mode 100644 index e6df21e1b..000000000 --- a/openstack-common.conf +++ /dev/null @@ -1,7 +0,0 @@ -[DEFAULT] - -# The list of modules to copy from openstack-common -modules=setup,cfg,iniparser,jsonutils,timeutils - -# The base module to hold the copy of openstack.common -base=keystoneclient diff --git a/playbooks/run-ds-tox.yaml b/playbooks/run-ds-tox.yaml new file mode 100644 index 000000000..b414b747c --- /dev/null +++ b/playbooks/run-ds-tox.yaml @@ -0,0 +1,5 @@ +- hosts: all + roles: + - run-devstack + - ensure-tox + - tox diff --git a/playbooks/tox-post.yaml b/playbooks/tox-post.yaml new file mode 100644 index 000000000..7f0cb1982 --- /dev/null +++ b/playbooks/tox-post.yaml @@ -0,0 +1,4 @@ +- hosts: all + roles: + - fetch-tox-output + - fetch-subunit-output diff --git a/releasenotes/notes/.placeholder b/releasenotes/notes/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/notes/Add-allow-expired-flag-to-validate-25b8914f4deb359b.yaml b/releasenotes/notes/Add-allow-expired-flag-to-validate-25b8914f4deb359b.yaml new file mode 100644 index 000000000..6a3f6cadd --- /dev/null +++ b/releasenotes/notes/Add-allow-expired-flag-to-validate-25b8914f4deb359b.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added a ``allow_expired`` argument to ``validate`` and ``get_token_data`` + in `keystoneclient.v3.tokens`. Setting this to ``True``, allos for a token + validation query to fetch expired tokens. diff --git a/releasenotes/notes/add-support-for-limits-6f883d6d3054a500.yaml b/releasenotes/notes/add-support-for-limits-6f883d6d3054a500.yaml new file mode 100644 index 000000000..623d96de2 --- /dev/null +++ b/releasenotes/notes/add-support-for-limits-6f883d6d3054a500.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added support for managing project-specific limits. The ``POST`` API for + limits in keystone supports batch creation, but the client implementation + does not. Creation for limits using the client must be done one at a time. diff --git a/releasenotes/notes/add-support-for-registered-limits-d83b888ea65a614b.yaml b/releasenotes/notes/add-support-for-registered-limits-d83b888ea65a614b.yaml new file mode 100644 index 000000000..114d95bce --- /dev/null +++ b/releasenotes/notes/add-support-for-registered-limits-d83b888ea65a614b.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added support for managing registered limits. The ``POST`` API for + registered limits in keystone supports batch creation, but the client + implementation does not. Creation of registered limits using the client + must be done one at a time. diff --git a/releasenotes/notes/bp-application-credentials-27728ded876d7d5a.yaml b/releasenotes/notes/bp-application-credentials-27728ded876d7d5a.yaml new file mode 100644 index 000000000..c67357c49 --- /dev/null +++ b/releasenotes/notes/bp-application-credentials-27728ded876d7d5a.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds support for creating, reading, and deleting application credentials. + With application credentials, a user can grant their applications limited + access to their cloud resources. Applications can use keystoneauth with + the `v3applicationcredential` auth plugin to authenticate with keystone + without needing the user's password. diff --git a/releasenotes/notes/bp-domain-config-9566e672a98f4e7f.yaml b/releasenotes/notes/bp-domain-config-9566e672a98f4e7f.yaml new file mode 100644 index 000000000..e6ae2b08d --- /dev/null +++ b/releasenotes/notes/bp-domain-config-9566e672a98f4e7f.yaml @@ -0,0 +1,7 @@ +--- +features: + - Added support for ``domain configs``. A user can now + upload domain specific configurations to keytone + using the client. See ``client.domain_configs.create``, + ``client.domain_configs.delete``, ``client.domain_configs.get`` + and ``client.domain_configs.update``. diff --git a/releasenotes/notes/bp-pci-dss-query-password-expired-users-b0c4b1bbdcf33f16.yaml b/releasenotes/notes/bp-pci-dss-query-password-expired-users-b0c4b1bbdcf33f16.yaml new file mode 100644 index 000000000..2699a7f5b --- /dev/null +++ b/releasenotes/notes/bp-pci-dss-query-password-expired-users-b0c4b1bbdcf33f16.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added ability to filter on multiple values with the same parameter key. + For example, we can now filter on user names that contain both ``test`` and + ``user`` using ``keystone.users.list(name__contains=['test', 'user'])``. diff --git a/releasenotes/notes/bp-whitelist-extension-for-app-creds-d03526e52e3edcce.yaml b/releasenotes/notes/bp-whitelist-extension-for-app-creds-d03526e52e3edcce.yaml new file mode 100644 index 000000000..9c7dc2bf1 --- /dev/null +++ b/releasenotes/notes/bp-whitelist-extension-for-app-creds-d03526e52e3edcce.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for creating access rules as an attribute of application + credentials as well as for retrieving and deleting them. diff --git a/releasenotes/notes/bug-1615076-26962c85aeaf288c.yaml b/releasenotes/notes/bug-1615076-26962c85aeaf288c.yaml new file mode 100644 index 000000000..6af51e4f1 --- /dev/null +++ b/releasenotes/notes/bug-1615076-26962c85aeaf288c.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - | + The region resource in Keystone never support or contain "enabled" property. + Thus the property is deprecated and will be removed in future versions. diff --git a/releasenotes/notes/bug-1616105-cc8b85eb056e99e2.yaml b/releasenotes/notes/bug-1616105-cc8b85eb056e99e2.yaml new file mode 100644 index 000000000..e9c1c9c3d --- /dev/null +++ b/releasenotes/notes/bug-1616105-cc8b85eb056e99e2.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - > + [`bug 1616105 `_] + Only log the response body when the ``Content-Type`` header is set to + ``application/json``. This avoids logging large binary objects (such as + images). Other ``Content-Type`` will not be logged. Additional + ``Content-Type`` strings can be added as required. diff --git a/releasenotes/notes/bug-1641674-4862454115265e76.yaml b/releasenotes/notes/bug-1641674-4862454115265e76.yaml new file mode 100644 index 000000000..19c8ecc34 --- /dev/null +++ b/releasenotes/notes/bug-1641674-4862454115265e76.yaml @@ -0,0 +1,8 @@ +--- +prelude: > + Keystone Client now supports endpoint group filtering. +features: + - | + Support for handling the relationship between endpoint groups and projects + has been added. It is now possible to list, associate, check and + disassociate endpoint groups that have access to a project. diff --git a/releasenotes/notes/bug-1654847-d2e9df994c7b617f.yaml b/releasenotes/notes/bug-1654847-d2e9df994c7b617f.yaml new file mode 100644 index 000000000..5d066e906 --- /dev/null +++ b/releasenotes/notes/bug-1654847-d2e9df994c7b617f.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + The ``X-Service-Token`` header value is now properly masked, and is + displayed as a hash value, in the log. diff --git a/releasenotes/notes/deprecated_auth-d2a2bf537bdb88d3.yaml b/releasenotes/notes/deprecated_auth-d2a2bf537bdb88d3.yaml new file mode 100644 index 000000000..fd08a910b --- /dev/null +++ b/releasenotes/notes/deprecated_auth-d2a2bf537bdb88d3.yaml @@ -0,0 +1,12 @@ +--- +deprecations: + - > + [`blueprint deprecate-to-ksa `_] + Several modules related to authentication in keystoneclient have been + deprecated in favor of [`keystoneauth `_] + These modules include: ``keystoneclient.session``, ``keystoneclient.adapter``, + ``keystoneclient.httpclient``, ``keystoneclient.auth.base``, + ``keystoneclient.auth.cli``, ``keystoneclient.auth.conf``, + ``keystoneclient.auth.identity.base``, and ``keystoneclient.auth.token_endpoint``. + Tips for migrating to `keystoneauth` have been + [`documented `_]. diff --git a/releasenotes/notes/drop-py-2-7-5ac18e82de83fcfa.yaml b/releasenotes/notes/drop-py-2-7-5ac18e82de83fcfa.yaml new file mode 100644 index 000000000..b97748489 --- /dev/null +++ b/releasenotes/notes/drop-py-2-7-5ac18e82de83fcfa.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + Python 2.7 support has been dropped. Last release of python-keystoneclient + to support python 2.7 is OpenStack Train. The minimum version of Python now + supported is Python 3.6. \ No newline at end of file diff --git a/releasenotes/notes/drop-python-3-6-and-3-7-ef1e107897dde8f4.yaml b/releasenotes/notes/drop-python-3-6-and-3-7-ef1e107897dde8f4.yaml new file mode 100644 index 000000000..db420d739 --- /dev/null +++ b/releasenotes/notes/drop-python-3-6-and-3-7-ef1e107897dde8f4.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Python 3.6 & 3.7 support has been dropped. The minimum version of Python now + supported is Python 3.8. diff --git a/releasenotes/notes/implied_roles-ea39d3c3d998d482.yaml b/releasenotes/notes/implied_roles-ea39d3c3d998d482.yaml new file mode 100644 index 000000000..e00ccae18 --- /dev/null +++ b/releasenotes/notes/implied_roles-ea39d3c3d998d482.yaml @@ -0,0 +1,3 @@ +--- +features: + - support for implied roles in v3 API. diff --git a/releasenotes/notes/ksc_2.1.0-739ded9c4c3f8aaa.yaml b/releasenotes/notes/ksc_2.1.0-739ded9c4c3f8aaa.yaml new file mode 100644 index 000000000..e95c939be --- /dev/null +++ b/releasenotes/notes/ksc_2.1.0-739ded9c4c3f8aaa.yaml @@ -0,0 +1,17 @@ +--- +fixes: + - > + [`bug 1462694 `_] + Add support for `include_subtree` in list_role_assignments. + - > + [`bug 1526686 `_] + Replace textwrap with faster code in cms functions. + - > + [`bug 1457702 `_] + Change default endpoint to public for keystone v3. + - > + [`bug 1520244 `_] + Support `truncated` flag returned from server. +other: + - > + Support v2 parameters for the v3 service create method. diff --git a/releasenotes/notes/list_projects_filtered_by_the_parent_project-a873974f197c1e37.yaml b/releasenotes/notes/list_projects_filtered_by_the_parent_project-a873974f197c1e37.yaml new file mode 100644 index 000000000..988dca5e3 --- /dev/null +++ b/releasenotes/notes/list_projects_filtered_by_the_parent_project-a873974f197c1e37.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Now keystone client supports to list projects which belongs to the given + parent project. diff --git a/releasenotes/notes/list_role_assignment_names-7e1b7eb8c2d22d7c.yaml b/releasenotes/notes/list_role_assignment_names-7e1b7eb8c2d22d7c.yaml new file mode 100644 index 000000000..499306a4a --- /dev/null +++ b/releasenotes/notes/list_role_assignment_names-7e1b7eb8c2d22d7c.yaml @@ -0,0 +1,6 @@ +--- +features: + - > + [`bug 1479569 `_] + With the ``include_names`` parameter set to True the names of the role assignments + are returned with the entities IDs. (GET /role_assignments?include_names=True) diff --git a/releasenotes/notes/project-tags-1f8a32d389951e7a.yaml b/releasenotes/notes/project-tags-1f8a32d389951e7a.yaml new file mode 100644 index 000000000..c0c868cbe --- /dev/null +++ b/releasenotes/notes/project-tags-1f8a32d389951e7a.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + [`blueprint project-tags `_] + The keystoneclient now supports project tags feature in keystone. This + allows operators to use the client to associate tags to a project, + retrieve tags associated with a project, delete tags associated with a + project, and filter projects based on tags. diff --git a/releasenotes/notes/remove-client-HTTPClient-b69cc6fb7d07fadc.yaml b/releasenotes/notes/remove-client-HTTPClient-b69cc6fb7d07fadc.yaml new file mode 100644 index 000000000..e02103073 --- /dev/null +++ b/releasenotes/notes/remove-client-HTTPClient-b69cc6fb7d07fadc.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + The deprecated ``keystoneclient.client.HTTPClient`` class has been removed. diff --git a/releasenotes/notes/remove-credentials-data-46ab3c3c248047cf.yaml b/releasenotes/notes/remove-credentials-data-46ab3c3c248047cf.yaml new file mode 100644 index 000000000..01ebe3e10 --- /dev/null +++ b/releasenotes/notes/remove-credentials-data-46ab3c3c248047cf.yaml @@ -0,0 +1,8 @@ +--- +prelude: > + The ``data`` argument for creating and updating credentials has + been removed. +other: + - The ``data`` argument for creating and updating credentials was + deprecated in the 1.7.0 release. It has been replaced by the + ``blob`` argument. diff --git a/releasenotes/notes/remove-middleware-eef8c40117b465aa.yaml b/releasenotes/notes/remove-middleware-eef8c40117b465aa.yaml new file mode 100644 index 000000000..cd9859cca --- /dev/null +++ b/releasenotes/notes/remove-middleware-eef8c40117b465aa.yaml @@ -0,0 +1,10 @@ +--- +prelude: > + keystoneclient.middleware has been removed. +critical: + - > + [`bug 1449066 `_] + The `keystoneclient.middleware` module has been removed in favor of the + keystonemiddleware library. The aforementioned module has been deprecated + since keystoneclient v0.10.0 which was included in the Juno release + of OpenStack. diff --git a/releasenotes/notes/remove-py38-2e39854190447827.yaml b/releasenotes/notes/remove-py38-2e39854190447827.yaml new file mode 100644 index 000000000..040316360 --- /dev/null +++ b/releasenotes/notes/remove-py38-2e39854190447827.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Support for Python 3.8 has been removed. Now the minimum python version + supported is 3.9 . diff --git a/releasenotes/notes/remove-py39-a294c2d7335b646e.yaml b/releasenotes/notes/remove-py39-a294c2d7335b646e.yaml new file mode 100644 index 000000000..eaf3014b9 --- /dev/null +++ b/releasenotes/notes/remove-py39-a294c2d7335b646e.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Support for Python 3.9 has been removed. Now Python 3.10 is the minimum + version supported. diff --git a/releasenotes/notes/remove_apiclient_exceptions-0cd5c8d16aa09a22.yaml b/releasenotes/notes/remove_apiclient_exceptions-0cd5c8d16aa09a22.yaml new file mode 100644 index 000000000..24a25d7e3 --- /dev/null +++ b/releasenotes/notes/remove_apiclient_exceptions-0cd5c8d16aa09a22.yaml @@ -0,0 +1,5 @@ +--- +other: + - > + Removed `keystoneclient.apiclient.exceptions`. This file was deprecated + in v0.7.1 and has now been replaced by `keystoneclient.exceptions`. diff --git a/releasenotes/notes/remove_apiclient_exceptions-6580003a885db286.yaml b/releasenotes/notes/remove_apiclient_exceptions-6580003a885db286.yaml new file mode 100644 index 000000000..1fbe6b72b --- /dev/null +++ b/releasenotes/notes/remove_apiclient_exceptions-6580003a885db286.yaml @@ -0,0 +1,10 @@ +--- +prelude: > + keystoneclient.apiclient has been removed. +critical: + - > + [`bug 1526651 `_] + The `keystoneclient.apiclient` module has been removed in favor of + `keystoneclient.exceptions`. The aforementioned module has been deprecated + since keystoneclient v0.7.1 which was inclued in the Juno release + of OpenStack. diff --git a/releasenotes/notes/remove_cli-d2c4435ba6a09b79.yaml b/releasenotes/notes/remove_cli-d2c4435ba6a09b79.yaml new file mode 100644 index 000000000..c1428096c --- /dev/null +++ b/releasenotes/notes/remove_cli-d2c4435ba6a09b79.yaml @@ -0,0 +1,7 @@ +--- +prelude: > + The ``keystone`` CLI has been removed. +other: + - The ``keystone`` CLI has been removed, using the ``openstack`` + CLI is recommended. This feature has been deprecated since the + Liberty release of Keystone. diff --git a/releasenotes/notes/removed-generic-client-ff505b2b01bc9302.yaml b/releasenotes/notes/removed-generic-client-ff505b2b01bc9302.yaml new file mode 100644 index 000000000..61b9d17ac --- /dev/null +++ b/releasenotes/notes/removed-generic-client-ff505b2b01bc9302.yaml @@ -0,0 +1,6 @@ +--- +deprecations: + - Deprecate the `keystoneclient.generic` client. This client used to be able + to determine available API versions and some basics around installed + extensions however the APIs were never upgraded for the v3 API. It doesn't + seem to be used in the openstack ecosystem. diff --git a/releasenotes/notes/return-request-id-to-caller-97fa269ad626f8c1.yaml b/releasenotes/notes/return-request-id-to-caller-97fa269ad626f8c1.yaml new file mode 100644 index 000000000..8ef4701db --- /dev/null +++ b/releasenotes/notes/return-request-id-to-caller-97fa269ad626f8c1.yaml @@ -0,0 +1,13 @@ +--- +features: + - > + [`blueprint return-request-id-to-caller + `_] + Instantiating client with ``include_metadata=True`` will cause manager response to return data + along with request_ids for better tracing. Refer [`using-api-v3 + `_] + + + Added support to return "x-openstack-request-id" header in request_ids attribute if + ``include_metadata=True``. Also, for APIs which return response as None, client will return request_ids + as well if ``include_metadata`` is True. \ No newline at end of file diff --git a/releasenotes/notes/switch-default-interface-v3-dcd7167196ace531.yaml b/releasenotes/notes/switch-default-interface-v3-dcd7167196ace531.yaml new file mode 100644 index 000000000..90709c097 --- /dev/null +++ b/releasenotes/notes/switch-default-interface-v3-dcd7167196ace531.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + For sessions using the v3 Identity API, the default interface has been + switched from ``admin`` to ``public``. This allows deployments to get rid + of the admin endpoint, which functionally is no longer necessary with the + v3 API. diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst new file mode 100644 index 000000000..2c9a36fae --- /dev/null +++ b/releasenotes/source/2023.1.rst @@ -0,0 +1,6 @@ +=========================== +2023.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: unmaintained/2023.1 diff --git a/releasenotes/source/2023.2.rst b/releasenotes/source/2023.2.rst new file mode 100644 index 000000000..a4838d7d0 --- /dev/null +++ b/releasenotes/source/2023.2.rst @@ -0,0 +1,6 @@ +=========================== +2023.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2023.2 diff --git a/releasenotes/source/2024.1.rst b/releasenotes/source/2024.1.rst new file mode 100644 index 000000000..6896656be --- /dev/null +++ b/releasenotes/source/2024.1.rst @@ -0,0 +1,6 @@ +=========================== +2024.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: unmaintained/2024.1 diff --git a/releasenotes/source/2024.2.rst b/releasenotes/source/2024.2.rst new file mode 100644 index 000000000..aaebcbc8c --- /dev/null +++ b/releasenotes/source/2024.2.rst @@ -0,0 +1,6 @@ +=========================== +2024.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2024.2 diff --git a/releasenotes/source/2025.1.rst b/releasenotes/source/2025.1.rst new file mode 100644 index 000000000..3add0e53a --- /dev/null +++ b/releasenotes/source/2025.1.rst @@ -0,0 +1,6 @@ +=========================== +2025.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.1 diff --git a/releasenotes/source/2025.2.rst b/releasenotes/source/2025.2.rst new file mode 100644 index 000000000..4dae18d86 --- /dev/null +++ b/releasenotes/source/2025.2.rst @@ -0,0 +1,6 @@ +=========================== +2025.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.2 diff --git a/releasenotes/source/2026.1.rst b/releasenotes/source/2026.1.rst new file mode 100644 index 000000000..3d2861580 --- /dev/null +++ b/releasenotes/source/2026.1.rst @@ -0,0 +1,6 @@ +=========================== +2026.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2026.1 diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 000000000..f2ae6a44e --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,262 @@ +# 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. + +# keystoneclient Release Notes documentation build configuration file, created +# by sphinx-quickstart on Tue Nov 3 17:40:50 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'openstackdocstheme', + 'reno.sphinxext', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +copyright = '2015, Keystone Developers' + +# Release notes are version independent. +# The full version, including alpha/beta/rc tags. +release = '' +# The short X.Y version. +version = '' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'native' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = '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 +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'KeystoneClientReleaseNotesdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'keystoneclientReleaseNotes.tex', + 'keystoneclient Release Notes Documentation', + 'Keystone Developers', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'keystoneclientreleasenotes', + 'keystoneclient Release Notes Documentation', + ['Keystone Developers'], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'keystoneclientReleaseNotes', + 'keystoneclient Release Notes Documentation', + 'Keystone Developers', 'keystoneclientReleaseNotes', + 'Python bindings for the OpenStack Identity service.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + +# -- Options for Internationalization output ------------------------------ +locale_dirs = ['locale/'] + +# -- Options for openstackdocstheme ------------------------------------------- +openstackdocs_repo_name = 'openstack/python-keystoneclient' +openstackdocs_bug_project = 'python-keystoneclient' +openstackdocs_bug_tag = '' diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 000000000..d84852efd --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,29 @@ +============================== + keystoneclient Release Notes +============================== + +.. toctree:: + :maxdepth: 1 + + unreleased + 2026.1 + 2025.2 + 2025.1 + 2024.2 + 2024.1 + 2023.2 + 2023.1 + zed + yoga + xena + wallaby + victoria + ussuri + train + stein + rocky + queens + pike + ocata + newton + mitaka diff --git a/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po new file mode 100644 index 000000000..aede2504e --- /dev/null +++ b/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po @@ -0,0 +1,57 @@ +# Gérald LONLAS , 2016. #zanata +msgid "" +msgstr "" +"Project-Id-Version: keystoneclient Release Notes 3.12.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-07-24 15:13+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2016-10-22 06:08+0000\n" +"Last-Translator: Gérald LONLAS \n" +"Language-Team: French\n" +"Language: fr\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" + +msgid "2.1.0" +msgstr "2.1.0" + +msgid "2.2.0" +msgstr "2.2.0" + +msgid "2.3.0" +msgstr "2.3.0" + +msgid "3.0.0" +msgstr "3.0.0" + +msgid "3.6.0" +msgstr "3.6.0" + +msgid "Bug Fixes" +msgstr "Corrections de bugs" + +msgid "Critical Issues" +msgstr "Erreurs critiques" + +msgid "Current Series Release Notes" +msgstr "Note de la release actuelle" + +msgid "Deprecation Notes" +msgstr "Notes dépréciées " + +msgid "Mitaka Series Release Notes" +msgstr "Note de release pour Mitaka" + +msgid "New Features" +msgstr "Nouvelles fonctionnalités" + +msgid "Newton Series Release Notes" +msgstr "Note de release pour Newton" + +msgid "Other Notes" +msgstr "Autres notes" + +msgid "keystoneclient Release Notes" +msgstr "Note de release pour keystoneclient" diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst new file mode 100644 index 000000000..e54560965 --- /dev/null +++ b/releasenotes/source/mitaka.rst @@ -0,0 +1,6 @@ +=================================== + Mitaka Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/mitaka diff --git a/releasenotes/source/newton.rst b/releasenotes/source/newton.rst new file mode 100644 index 000000000..7b7d7352b --- /dev/null +++ b/releasenotes/source/newton.rst @@ -0,0 +1,6 @@ +============================= + Newton Series Release Notes +============================= + +.. release-notes:: + :branch: origin/stable/newton diff --git a/releasenotes/source/ocata.rst b/releasenotes/source/ocata.rst new file mode 100644 index 000000000..9515f6cf0 --- /dev/null +++ b/releasenotes/source/ocata.rst @@ -0,0 +1,6 @@ +============================ + Ocata Series Release Notes +============================ + +.. release-notes:: + :branch: origin/stable/ocata diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst new file mode 100644 index 000000000..e43bfc0ce --- /dev/null +++ b/releasenotes/source/pike.rst @@ -0,0 +1,6 @@ +=================================== + Pike Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/pike diff --git a/releasenotes/source/queens.rst b/releasenotes/source/queens.rst new file mode 100644 index 000000000..36ac6160c --- /dev/null +++ b/releasenotes/source/queens.rst @@ -0,0 +1,6 @@ +=================================== + Queens Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/queens diff --git a/releasenotes/source/rocky.rst b/releasenotes/source/rocky.rst new file mode 100644 index 000000000..40dd517b7 --- /dev/null +++ b/releasenotes/source/rocky.rst @@ -0,0 +1,6 @@ +=================================== + Rocky Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/rocky diff --git a/releasenotes/source/stein.rst b/releasenotes/source/stein.rst new file mode 100644 index 000000000..efaceb667 --- /dev/null +++ b/releasenotes/source/stein.rst @@ -0,0 +1,6 @@ +=================================== + Stein Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/stein diff --git a/releasenotes/source/train.rst b/releasenotes/source/train.rst new file mode 100644 index 000000000..7fa1088ac --- /dev/null +++ b/releasenotes/source/train.rst @@ -0,0 +1,6 @@ +=================================== + Train Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/train diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 000000000..cd22aabcc --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================== + Current Series Release Notes +============================== + +.. release-notes:: diff --git a/releasenotes/source/ussuri.rst b/releasenotes/source/ussuri.rst new file mode 100644 index 000000000..e21e50e0c --- /dev/null +++ b/releasenotes/source/ussuri.rst @@ -0,0 +1,6 @@ +=========================== +Ussuri Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/ussuri diff --git a/releasenotes/source/victoria.rst b/releasenotes/source/victoria.rst new file mode 100644 index 000000000..8ce933419 --- /dev/null +++ b/releasenotes/source/victoria.rst @@ -0,0 +1,6 @@ +============================= +Victoria Series Release Notes +============================= + +.. release-notes:: + :branch: unmaintained/victoria diff --git a/releasenotes/source/wallaby.rst b/releasenotes/source/wallaby.rst new file mode 100644 index 000000000..bcf35c5f8 --- /dev/null +++ b/releasenotes/source/wallaby.rst @@ -0,0 +1,6 @@ +============================ +Wallaby Series Release Notes +============================ + +.. release-notes:: + :branch: unmaintained/wallaby diff --git a/releasenotes/source/xena.rst b/releasenotes/source/xena.rst new file mode 100644 index 000000000..d19eda488 --- /dev/null +++ b/releasenotes/source/xena.rst @@ -0,0 +1,6 @@ +========================= +Xena Series Release Notes +========================= + +.. release-notes:: + :branch: unmaintained/xena diff --git a/releasenotes/source/yoga.rst b/releasenotes/source/yoga.rst new file mode 100644 index 000000000..43cafdea8 --- /dev/null +++ b/releasenotes/source/yoga.rst @@ -0,0 +1,6 @@ +========================= +Yoga Series Release Notes +========================= + +.. release-notes:: + :branch: unmaintained/yoga diff --git a/releasenotes/source/zed.rst b/releasenotes/source/zed.rst new file mode 100644 index 000000000..6cc2b1554 --- /dev/null +++ b/releasenotes/source/zed.rst @@ -0,0 +1,6 @@ +======================== +Zed Series Release Notes +======================== + +.. release-notes:: + :branch: unmaintained/zed diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..1b69b9833 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +# Requirements lower bounds listed here are our best effort to keep them up to +# date but we do not test them so no guarantee of having them all correct. If +# you find any incorrect lower bounds, let us know or propose a fix. +pbr>=2.0.0 # Apache-2.0 + +debtcollector>=1.2.0 # Apache-2.0 +keystoneauth1>=3.4.0 # Apache-2.0 +oslo.config>=5.2.0 # Apache-2.0 +oslo.i18n>=3.15.3 # Apache-2.0 +oslo.serialization>=2.18.0 # Apache-2.0 +oslo.utils>=3.33.0 # Apache-2.0 +requests>=2.14.2 # Apache-2.0 +stevedore>=1.20.0 # Apache-2.0 +packaging>=20.4 # BSD diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index 782c36561..000000000 --- a/run_tests.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/bin/bash - -set -eu - -function usage { - echo "Usage: $0 [OPTION]..." - echo "Run python-keystoneclient test suite" - echo "" - echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" - echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" - echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment" - echo " -x, --stop Stop running tests after the first error or failure." - echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." - echo " -p, --pep8 Just run pep8" - echo " -P, --no-pep8 Don't run pep8" - echo " -c, --coverage Generate coverage report" - echo " -h, --help Print this usage message" - echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list" - echo "" - echo "Note: with no options specified, the script will try to run the tests in a virtual environment," - echo " If no virtualenv is found, the script will ask if you would like to create one. If you " - echo " prefer to run tests NOT in a virtual environment, simply pass the -N option." - exit -} - -function process_option { - case "$1" in - -h|--help) usage;; - -V|--virtual-env) always_venv=1; never_venv=0;; - -N|--no-virtual-env) always_venv=0; never_venv=1;; - -s|--no-site-packages) no_site_packages=1;; - -f|--force) force=1;; - -p|--pep8) just_pep8=1;; - -P|--no-pep8) no_pep8=1;; - -c|--coverage) coverage=1;; - -*) noseopts="$noseopts $1";; - *) noseargs="$noseargs $1" - esac -} - -venv=.venv -with_venv=tools/with_venv.sh -always_venv=0 -never_venv=0 -force=0 -no_site_packages=0 -installvenvopts= -noseargs= -noseopts= -wrapper="" -just_pep8=0 -no_pep8=0 -coverage=0 - -for arg in "$@"; do - process_option $arg -done - -# If enabled, tell nose to collect coverage data -if [ $coverage -eq 1 ]; then - noseopts="$noseopts --with-coverage --cover-package=keystoneclient" -fi - -if [ $no_site_packages -eq 1 ]; then - installvenvopts="--no-site-packages" -fi - -function run_tests { - # Just run the test suites in current environment - ${wrapper} $NOSETESTS - # If we get some short import error right away, print the error log directly - RESULT=$? - return $RESULT -} - -function run_pep8 { - echo "Running pep8 ..." - srcfiles="keystoneclient tests" - # Just run PEP8 in current environment - # - # NOTE(heckj): E125, E126 are being ignored matching other openstack projects - # for pep 1.3.3 due to relatively arbitrary line indentation rulings - ${wrapper} pep8 --repeat --show-pep8 --show-source \ - --ignore=E125,E126 --exclude=.venv,.tox,dist,doc \ - ${srcfiles} -} - -NOSETESTS="nosetests $noseopts $noseargs" - -if [ $never_venv -eq 0 ] -then - # Remove the virtual environment if --force used - if [ $force -eq 1 ]; then - echo "Cleaning virtualenv..." - rm -rf ${venv} - fi - if [ -e ${venv} ]; then - wrapper="${with_venv}" - else - if [ $always_venv -eq 1 ]; then - # Automatically install the virtualenv - python tools/install_venv.py $installvenvopts - wrapper="${with_venv}" - else - echo -e "No virtual environment found...create one? (Y/n) \c" - read use_ve - if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then - # Install the virtualenv and run the test suite in it - python tools/install_venv.py $installvenvopts - wrapper=${with_venv} - fi - fi - fi -fi - -# Delete old coverage data from previous runs -if [ $coverage -eq 1 ]; then - ${wrapper} coverage erase -fi - -if [ $just_pep8 -eq 1 ]; then - run_pep8 - exit -fi - -run_tests - -# NOTE(sirp): we only want to run pep8 when we're running the full-test suite, -# not when we're running tests individually. To handle this, we need to -# distinguish between options (noseopts), which begin with a '-', and -# arguments (noseargs). -if [ -z "$noseargs" ]; then - if [ $no_pep8 -eq 0 ]; then - run_pep8 - fi -fi - -if [ $coverage -eq 1 ]; then - echo "Generating coverage report in covhtml/" - ${wrapper} coverage html -d covhtml -i -fi diff --git a/setup.cfg b/setup.cfg index 342110126..037fe1c57 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,28 +1,38 @@ -[nosetests] -verbosity=2 -detailed-errors=1 -cover-package = keystoneclient -cover-erase = true -cover-inclusive = true +[metadata] +name = python-keystoneclient +summary = Client Library for OpenStack Identity +description_file = + README.rst +author = OpenStack +author_email = openstack-discuss@lists.openstack.org +home_page = https://docs.openstack.org/python-keystoneclient/latest/ +python_requires = >=3.10 +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 -[build_sphinx] -source-dir = doc/source -build-dir = doc/build -all_files = 1 +[files] +packages = + keystoneclient -[upload_sphinx] -upload-dir = doc/build/html - -[compile_catalog] -directory = keystoneclient/locale -domain = keystoneclient - -[update_catalog] -domain = keystoneclient -output_dir = keystoneclient/locale -input_file = keystoneclient/locale/keystoneclient.pot - -[extract_messages] -keywords = _ gettext ngettext l_ lazy_gettext -mapping_file = babel.cfg -output_file = keystoneclient/locale/keystoneclient.pot +[entry_points] +keystoneclient.auth.plugin = + password = keystoneclient.auth.identity.generic:Password + token = keystoneclient.auth.identity.generic:Token + admin_token = keystoneclient.auth.token_endpoint:Token + v2password = keystoneclient.auth.identity.v2:Password + v2token = keystoneclient.auth.identity.v2:Token + v3password = keystoneclient.auth.identity.v3:Password + v3token = keystoneclient.auth.identity.v3:Token + v3oidcpassword = keystoneclient.contrib.auth.v3.oidc:OidcPassword + v3unscopedsaml = keystoneclient.contrib.auth.v3.saml2:Saml2UnscopedToken + v3scopedsaml = keystoneclient.contrib.auth.v3.saml2:Saml2ScopedToken + v3unscopedadfs = keystoneclient.contrib.auth.v3.saml2:ADFSUnscopedToken diff --git a/setup.py b/setup.py index 82ca7303b..cd35c3c35 100644 --- a/setup.py +++ b/setup.py @@ -1,46 +1,20 @@ -import os -import sys -import setuptools - -from keystoneclient.openstack.common import setup - - -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. -requires = setup.parse_requirements() -depend_links = setup.parse_dependency_links() -tests_require = setup.parse_requirements(['tools/test-requires']) +import setuptools setuptools.setup( - name="python-keystoneclient", - version=setup.get_post_version('keystoneclient'), - description="Client library for OpenStack Identity API (Keystone)", - long_description=read('README.rst'), - url='https://github.com/openstack/python-keystoneclient', - license='Apache', - author='Nebula Inc, based on work by Rackspace and Jacob Kaplan-Moss', - author_email='gabriel.hurley@nebula.com', - packages=setuptools.find_packages(exclude=['tests', 'tests.*']), - classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Environment :: OpenStack', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - ], - install_requires=requires, - dependency_links=depend_links, - cmdclass=setup.get_cmdclass(), - - tests_require=tests_require, - test_suite="nose.collector", - - entry_points={ - 'console_scripts': ['keystone = keystoneclient.shell:main'] - }, - data_files=[('keystoneclient', ['keystoneclient/versioninfo'])], -) + setup_requires=['pbr>=2.0.0'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..238690c65 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,18 @@ +hacking>=6.1.0,<6.2.0 # Apache-2.0 + +coverage>=4.0 # Apache-2.0 +fixtures>=3.0.0 # Apache-2.0/BSD +keyring>=5.5.1 # MIT/PSF +lxml>=4.5.0 # BSD +oauthlib>=0.6.2 # BSD +openstacksdk>=0.10.0 # Apache-2.0 +oslotest>=3.2.0 # Apache-2.0 +requests-mock>=1.2.0 # Apache-2.0 +tempest>=17.1.0 # Apache-2.0 +stestr>=2.0.0 # Apache-2.0 +testresources>=2.0.0 # Apache-2.0/BSD +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=2.2.0 # MIT + +# Bandit security code scanner +bandit>=1.1.0 # Apache-2.0 diff --git a/tests/client_fixtures.py b/tests/client_fixtures.py deleted file mode 100644 index f0b513795..000000000 --- a/tests/client_fixtures.py +++ /dev/null @@ -1,72 +0,0 @@ -UNSCOPED_TOKEN = { - u'access': {u'serviceCatalog': {}, - u'token': {u'expires': u'2012-10-03T16:58:01Z', - u'id': u'3e2813b7ba0b4006840c3825860b86ed'}, - u'user': {u'id': u'c4da488862bd435c9e6c0275a0d0e49a', - u'name': u'exampleuser', - u'roles': [], - u'roles_links': [], - u'username': u'exampleuser'} - } -} - -PROJECT_SCOPED_TOKEN = { - u'access': { - u'serviceCatalog': [{ - u'endpoints': [{ - u'adminURL': u'http://admin:8776/v1/225da22d3ce34b15877ea70b2a575f58', - u'internalURL': - u'http://internal:8776/v1/225da22d3ce34b15877ea70b2a575f58', - u'publicURL': - u'http://public.com:8776/v1/225da22d3ce34b15877ea70b2a575f58', - u'region': u'RegionOne' - }], - u'endpoints_links': [], - u'name': u'Volume Service', - u'type': u'volume'}, - {u'endpoints': [{ - u'adminURL': u'http://admin:9292/v1', - u'internalURL': u'http://internal:9292/v1', - u'publicURL': u'http://public.com:9292/v1', - u'region': u'RegionOne'}], - u'endpoints_links': [], - u'name': u'Image Service', - u'type': u'image'}, - {u'endpoints': [{ -u'adminURL': u'http://admin:8774/v2/225da22d3ce34b15877ea70b2a575f58', -u'internalURL': u'http://internal:8774/v2/225da22d3ce34b15877ea70b2a575f58', -u'publicURL': u'http://public.com:8774/v2/225da22d3ce34b15877ea70b2a575f58', -u'region': u'RegionOne'}], - u'endpoints_links': [], - u'name': u'Compute Service', - u'type': u'compute'}, - {u'endpoints': [{ -u'adminURL': u'http://admin:8773/services/Admin', -u'internalURL': u'http://internal:8773/services/Cloud', -u'publicURL': u'http://public.com:8773/services/Cloud', -u'region': u'RegionOne'}], - u'endpoints_links': [], - u'name': u'EC2 Service', - u'type': u'ec2'}, - {u'endpoints': [{ -u'adminURL': u'http://admin:35357/v2.0', -u'internalURL': u'http://internal:5000/v2.0', -u'publicURL': u'http://public.com:5000/v2.0', -u'region': u'RegionOne'}], - u'endpoints_links': [], - u'name': u'Identity Service', - u'type': u'identity'}], - u'token': {u'expires': u'2012-10-03T16:53:36Z', - u'id': u'04c7d5ffaeef485f9dc69c06db285bdb', - u'tenant': {u'description': u'', - u'enabled': True, - u'id': u'225da22d3ce34b15877ea70b2a575f58', - u'name': u'exampleproject'}}, - u'user': {u'id': u'c4da488862bd435c9e6c0275a0d0e49a', - u'name': u'exampleuser', - u'roles': [{u'id': u'edc12489faa74ee0aca0b8a0b4d74a74', - u'name': u'Member'}], - u'roles_links': [], - u'username': u'exampleuser'} - } -} diff --git a/tests/test_access.py b/tests/test_access.py deleted file mode 100644 index 621fefc5a..000000000 --- a/tests/test_access.py +++ /dev/null @@ -1,69 +0,0 @@ -import datetime - -from keystoneclient import access -from keystoneclient.openstack.common import timeutils -from tests import utils -from tests import client_fixtures - -UNSCOPED_TOKEN = client_fixtures.UNSCOPED_TOKEN -PROJECT_SCOPED_TOKEN = client_fixtures.PROJECT_SCOPED_TOKEN - - -class AccessInfoTest(utils.TestCase): - def test_building_unscoped_accessinfo(self): - auth_ref = access.AccessInfo(UNSCOPED_TOKEN['access']) - - self.assertTrue(auth_ref) - self.assertIn('token', auth_ref) - self.assertIn('serviceCatalog', auth_ref) - self.assertFalse(auth_ref['serviceCatalog']) - - self.assertEquals(auth_ref.auth_token, - '3e2813b7ba0b4006840c3825860b86ed') - self.assertEquals(auth_ref.username, 'exampleuser') - self.assertEquals(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') - - self.assertEquals(auth_ref.tenant_name, None) - self.assertEquals(auth_ref.tenant_id, None) - - self.assertEquals(auth_ref.auth_url, None) - self.assertEquals(auth_ref.management_url, None) - - self.assertFalse(auth_ref.scoped) - - self.assertEquals(auth_ref.expires, timeutils.parse_isotime( - UNSCOPED_TOKEN['access']['token']['expires'])) - - def test_will_expire_soon(self): - expires = timeutils.utcnow() + datetime.timedelta(minutes=5) - UNSCOPED_TOKEN['access']['token']['expires'] = expires.isoformat() - auth_ref = access.AccessInfo(UNSCOPED_TOKEN['access']) - self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) - self.assertTrue(auth_ref.will_expire_soon(stale_duration=300)) - - def test_building_scoped_accessinfo(self): - auth_ref = access.AccessInfo(PROJECT_SCOPED_TOKEN['access']) - - self.assertTrue(auth_ref) - self.assertIn('token', auth_ref) - self.assertIn('serviceCatalog', auth_ref) - self.assertTrue(auth_ref['serviceCatalog']) - - self.assertEquals(auth_ref.auth_token, - '04c7d5ffaeef485f9dc69c06db285bdb') - self.assertEquals(auth_ref.username, 'exampleuser') - self.assertEquals(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') - - self.assertEquals(auth_ref.tenant_name, 'exampleproject') - self.assertEquals(auth_ref.tenant_id, - '225da22d3ce34b15877ea70b2a575f58') - - self.assertEquals(auth_ref.tenant_name, auth_ref.project_name) - self.assertEquals(auth_ref.tenant_id, auth_ref.project_id) - - self.assertEquals(auth_ref.auth_url, - ('http://public.com:5000/v2.0',)) - self.assertEquals(auth_ref.management_url, - ('http://admin:35357/v2.0',)) - - self.assertTrue(auth_ref.scoped) diff --git a/tests/test_auth_token_middleware.py b/tests/test_auth_token_middleware.py deleted file mode 100644 index 40fc4018f..000000000 --- a/tests/test_auth_token_middleware.py +++ /dev/null @@ -1,910 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime -import iso8601 -import os -import string -import tempfile -import testtools - -import webob - -from keystoneclient.common import cms -from keystoneclient import utils -from keystoneclient.middleware import auth_token -from keystoneclient.middleware import memcache_crypt -from keystoneclient.openstack.common import jsonutils -from keystoneclient.openstack.common import timeutils -from keystoneclient.middleware import test - - -CERTDIR = test.rootdir("python-keystoneclient/examples/pki/certs") -KEYDIR = test.rootdir("python-keystoneclient/examples/pki/private") -CMSDIR = test.rootdir("python-keystoneclient/examples/pki/cms") -SIGNING_CERT = os.path.join(CERTDIR, 'signing_cert.pem') -SIGNING_KEY = os.path.join(KEYDIR, 'signing_key.pem') -CA = os.path.join(CERTDIR, 'ca.pem') - -REVOCATION_LIST = None -REVOKED_TOKEN = None -REVOKED_TOKEN_HASH = None -SIGNED_REVOCATION_LIST = None -SIGNED_TOKEN_SCOPED = None -SIGNED_TOKEN_UNSCOPED = None -SIGNED_TOKEN_SCOPED_KEY = None -SIGNED_TOKEN_UNSCOPED_KEY = None - -VALID_SIGNED_REVOCATION_LIST = None - -UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d" -UUID_TOKEN_NO_SERVICE_CATALOG = '8286720fbe4941e69fa8241723bb02df' -UUID_TOKEN_UNSCOPED = '731f903721c14827be7b2dc912af7776' -VALID_DIABLO_TOKEN = 'b0cf19b55dbb4f20a6ee18e6c6cf1726' - -INVALID_SIGNED_TOKEN = string.replace( - """AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB -CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC -DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD -EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE -FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -0000000000000000000000000000000000000000000000000000000000000000 -1111111111111111111111111111111111111111111111111111111111111111 -2222222222222222222222222222222222222222222222222222222222222222 -3333333333333333333333333333333333333333333333333333333333333333 -4444444444444444444444444444444444444444444444444444444444444444 -5555555555555555555555555555555555555555555555555555555555555555 -6666666666666666666666666666666666666666666666666666666666666666 -7777777777777777777777777777777777777777777777777777777777777777 -8888888888888888888888888888888888888888888888888888888888888888 -9999999999999999999999999999999999999999999999999999999999999999 -0000000000000000000000000000000000000000000000000000000000000000 -xg==""", "\n", "") - -# JSON responses keyed by token ID -TOKEN_RESPONSES = { - UUID_TOKEN_DEFAULT: { - 'access': { - 'token': { - 'id': UUID_TOKEN_DEFAULT, - 'expires': '2999-01-01T00:00:10Z', - 'tenant': { - 'id': 'tenant_id1', - 'name': 'tenant_name1', - }, - }, - 'user': { - 'id': 'user_id1', - 'name': 'user_name1', - 'roles': [ - {'name': 'role1'}, - {'name': 'role2'}, - ], - }, - 'serviceCatalog': {} - }, - }, - VALID_DIABLO_TOKEN: { - 'access': { - 'token': { - 'id': VALID_DIABLO_TOKEN, - 'expires': '2999-01-01T00:00:10', - 'tenantId': 'tenant_id1', - }, - 'user': { - 'id': 'user_id1', - 'name': 'user_name1', - 'roles': [ - {'name': 'role1'}, - {'name': 'role2'}, - ], - }, - }, - }, - UUID_TOKEN_UNSCOPED: { - 'access': { - 'token': { - 'id': UUID_TOKEN_UNSCOPED, - 'expires': '2999-01-01T00:00:10Z', - }, - 'user': { - 'id': 'user_id1', - 'name': 'user_name1', - 'roles': [ - {'name': 'role1'}, - {'name': 'role2'}, - ], - }, - }, - }, - UUID_TOKEN_NO_SERVICE_CATALOG: { - 'access': { - 'token': { - 'id': 'valid-token', - 'expires': '2999-01-01T00:00:10Z', - 'tenant': { - 'id': 'tenant_id1', - 'name': 'tenant_name1', - }, - }, - 'user': { - 'id': 'user_id1', - 'name': 'user_name1', - 'roles': [ - {'name': 'role1'}, - {'name': 'role2'}, - ], - } - }, - }, -} - -FAKE_RESPONSE_STACK = [] - - -# The data for these tests are signed using openssl and are stored in files -# in the signing subdirectory. In order to keep the values consistent between -# the tests and the signed documents, we read them in for use in the tests. -def setUpModule(self): - signing_path = CMSDIR - with open(os.path.join(signing_path, 'auth_token_scoped.pem')) as f: - self.SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read()) - with open(os.path.join(signing_path, 'auth_token_unscoped.pem')) as f: - self.SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read()) - with open(os.path.join(signing_path, 'auth_token_revoked.pem')) as f: - self.REVOKED_TOKEN = cms.cms_to_token(f.read()) - self.REVOKED_TOKEN_HASH = utils.hash_signed_token(self.REVOKED_TOKEN) - with open(os.path.join(signing_path, 'revocation_list.json')) as f: - self.REVOCATION_LIST = jsonutils.loads(f.read()) - with open(os.path.join(signing_path, 'revocation_list.pem')) as f: - self.VALID_SIGNED_REVOCATION_LIST = jsonutils.dumps( - {'signed': f.read()}) - self.SIGNED_TOKEN_SCOPED_KEY =\ - cms.cms_hash_token(self.SIGNED_TOKEN_SCOPED) - self.SIGNED_TOKEN_UNSCOPED_KEY =\ - cms.cms_hash_token(self.SIGNED_TOKEN_UNSCOPED) - - self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_KEY] = { - 'access': { - 'token': { - 'id': self.SIGNED_TOKEN_SCOPED_KEY, - }, - 'user': { - 'id': 'user_id1', - 'name': 'user_name1', - 'tenantId': 'tenant_id1', - 'tenantName': 'tenant_name1', - 'roles': [ - {'name': 'role1'}, - {'name': 'role2'}, - ], - }, - }, - } - - self.TOKEN_RESPONSES[SIGNED_TOKEN_UNSCOPED_KEY] = { - 'access': { - 'token': { - 'id': SIGNED_TOKEN_UNSCOPED_KEY, - }, - 'user': { - 'id': 'user_id1', - 'name': 'user_name1', - 'roles': [ - {'name': 'role1'}, - {'name': 'role2'}, - ], - }, - }, - }, - - -class FakeMemcache(object): - def __init__(self): - self.set_key = None - self.set_value = None - self.token_expiration = None - - def get(self, key): - data = TOKEN_RESPONSES[SIGNED_TOKEN_SCOPED_KEY].copy() - if not data or key != "tokens/%s" % (data['access']['token']['id']): - return - if not self.token_expiration: - dt = datetime.datetime.now() + datetime.timedelta(minutes=5) - self.token_expiration = dt.strftime("%s") - dt = datetime.datetime.now() + datetime.timedelta(hours=24) - ks_expires = dt.isoformat() - data['access']['token']['expires'] = ks_expires - return (data, str(self.token_expiration)) - - def set(self, key, value, time=None): - self.set_value = value - self.set_key = key - - -class FakeSwiftMemcacheRing(object): - def __init__(self): - self.set_key = None - self.set_value = None - self.token_expiration = None - - def get(self, key): - data = TOKEN_RESPONSES[SIGNED_TOKEN_SCOPED_KEY].copy() - if not data or key != "tokens/%s" % (data['access']['token']['id']): - return - if not self.token_expiration: - dt = datetime.datetime.now() + datetime.timedelta(minutes=5) - self.token_expiration = dt.strftime("%s") - dt = datetime.datetime.now() + datetime.timedelta(hours=24) - ks_expires = dt.isoformat() - data['access']['token']['expires'] = ks_expires - return (data, str(self.token_expiration)) - - def set(self, key, value, serialize=True, timeout=0): - self.set_value = value - self.set_key = key - - -class FakeHTTPResponse(object): - def __init__(self, status, body): - self.status = status - self.body = body - - def read(self): - return self.body - - -class FakeStackHTTPConnection(object): - - def __init__(self, *args, **kwargs): - pass - - def getresponse(self): - if len(FAKE_RESPONSE_STACK): - return FAKE_RESPONSE_STACK.pop() - return FakeHTTPResponse(500, jsonutils.dumps('UNEXPECTED RESPONSE')) - - def request(self, *_args, **_kwargs): - pass - - def close(self): - pass - - -class FakeHTTPConnection(object): - - last_requested_url = '' - - def __init__(self, *args): - self.send_valid_revocation_list = True - - def request(self, method, path, **kwargs): - """Fakes out several http responses. - - If a POST request is made, we assume the calling code is trying - to get a new admin token. - - If a GET request is made to validate a token, return success - if the token is 'token1'. If a different token is provided, return - a 404, indicating an unknown (therefore unauthorized) token. - - """ - FakeHTTPConnection.last_requested_url = path - if method == 'POST': - status = 200 - body = jsonutils.dumps({ - 'access': { - 'token': {'id': 'admin_token2'}, - }, - }) - - else: - token_id = path.rsplit('/', 1)[1] - if token_id in TOKEN_RESPONSES.keys(): - status = 200 - body = jsonutils.dumps(TOKEN_RESPONSES[token_id]) - elif token_id == "revoked": - status = 200 - body = SIGNED_REVOCATION_LIST - else: - status = 404 - body = str() - - self.resp = FakeHTTPResponse(status, body) - - def getresponse(self): - return self.resp - - def close(self): - pass - - -class FakeApp(object): - """This represents a WSGI app protected by the auth_token middleware.""" - def __init__(self, expected_env=None): - expected_env = expected_env or {} - self.expected_env = { - 'HTTP_X_IDENTITY_STATUS': 'Confirmed', - 'HTTP_X_TENANT_ID': 'tenant_id1', - 'HTTP_X_TENANT_NAME': 'tenant_name1', - 'HTTP_X_USER_ID': 'user_id1', - 'HTTP_X_USER_NAME': 'user_name1', - 'HTTP_X_ROLES': 'role1,role2', - 'HTTP_X_USER': 'user_name1', # deprecated (diablo-compat) - 'HTTP_X_TENANT': 'tenant_name1', # deprecated (diablo-compat) - 'HTTP_X_ROLE': 'role1,role2', # deprecated (diablo-compat) - } - self.expected_env.update(expected_env) - - def __call__(self, env, start_response): - for k, v in self.expected_env.items(): - assert env[k] == v, '%s != %s' % (env[k], v) - - resp = webob.Response() - resp.body = 'SUCCESS' - return resp(env, start_response) - - -class BaseAuthTokenMiddlewareTest(testtools.TestCase): - - def setUp(self, expected_env=None): - super(BaseAuthTokenMiddlewareTest, self).setUp() - expected_env = expected_env or {} - - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'auth_admin_prefix': '/testadmin', - 'signing_dir': CERTDIR, - } - - self.middleware = auth_token.AuthProtocol(FakeApp(expected_env), conf) - self.middleware.http_client_class = FakeHTTPConnection - self.middleware._iso8601 = iso8601 - - self.response_status = None - self.response_headers = None - self.middleware.revoked_file_name = tempfile.mkstemp()[1] - cache_timeout = datetime.timedelta(days=1) - self.middleware.token_revocation_list_cache_timeout = cache_timeout - self.middleware.token_revocation_list = jsonutils.dumps( - {"revoked": [], "extra": "success"}) - - signed_list = 'SIGNED_REVOCATION_LIST' - valid_signed_list = 'VALID_SIGNED_REVOCATION_LIST' - globals()[signed_list] = globals()[valid_signed_list] - - super(BaseAuthTokenMiddlewareTest, self).setUp() - - def tearDown(self): - super(BaseAuthTokenMiddlewareTest, self).tearDown() - try: - os.remove(self.middleware.revoked_file_name) - except OSError: - pass - - def start_fake_response(self, status, headers): - self.response_status = int(status.split(' ', 1)[0]) - self.response_headers = dict(headers) - - -class StackResponseAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): - """Auth Token middleware test setup that allows the tests to define - a stack of responses to HTTP requests in the test and get those - responses back in sequence for testing. - - Example:: - - resp1 = FakeHTTPResponse(401, jsonutils.dumps('')) - resp2 = FakeHTTPResponse(200, jsonutils.dumps({ - 'access': { - 'token': {'id': 'admin_token2'}, - }, - }) - FAKE_RESPONSE_STACK.append(resp1) - FAKE_RESPONSE_STACK.append(resp2) - - ... do your testing code here ... - - """ - - def setUp(self, expected_env=None): - super(StackResponseAuthTokenMiddlewareTest, self).setUp(expected_env) - self.middleware.http_client_class = FakeStackHTTPConnection - - def test_fetch_revocation_list_with_expire(self): - # first response to revocation list should return 401 Unauthorized - # to pretend to be an expired token - resp1 = FakeHTTPResponse(200, jsonutils.dumps({ - 'access': { - 'token': {'id': 'admin_token2'}, - }, - })) - resp2 = FakeHTTPResponse(401, jsonutils.dumps('')) - resp3 = FakeHTTPResponse(200, jsonutils.dumps({ - 'access': { - 'token': {'id': 'admin_token2'}, - }, - })) - resp4 = FakeHTTPResponse(200, SIGNED_REVOCATION_LIST) - - # first get_admin_token() call - FAKE_RESPONSE_STACK.append(resp1) - # request revocation list, get "unauthorized" due to simulated expired - # token - FAKE_RESPONSE_STACK.append(resp2) - # request a new admin_token - FAKE_RESPONSE_STACK.append(resp3) - # request revocation list, get the revocation list properly - FAKE_RESPONSE_STACK.append(resp4) - - fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) - self.assertEqual(fetched_list, REVOCATION_LIST) - - -class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): - """Auth Token middleware should understand Diablo keystone responses.""" - def setUp(self): - # pre-diablo only had Tenant ID, which was also the Name - expected_env = { - 'HTTP_X_TENANT_ID': 'tenant_id1', - 'HTTP_X_TENANT_NAME': 'tenant_id1', - # now deprecated (diablo-compat) - 'HTTP_X_TENANT': 'tenant_id1', - } - super(DiabloAuthTokenMiddlewareTest, self).setUp(expected_env) - - def test_valid_diablo_response(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = VALID_DIABLO_TOKEN - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - self.assertTrue('keystone.token_info' in req.environ) - - -class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): - def assert_valid_request_200(self, token): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = token - body = self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - self.assertTrue(req.headers.get('X-Service-Catalog')) - self.assertEqual(body, ['SUCCESS']) - self.assertTrue('keystone.token_info' in req.environ) - - def test_valid_uuid_request(self): - self.assert_valid_request_200(UUID_TOKEN_DEFAULT) - self.assertEqual("/testadmin/v2.0/tokens/%s" % UUID_TOKEN_DEFAULT, - FakeHTTPConnection.last_requested_url) - - def test_valid_signed_request(self): - FakeHTTPConnection.last_requested_url = '' - self.assert_valid_request_200(SIGNED_TOKEN_SCOPED) - self.assertEqual(self.middleware.conf['auth_admin_prefix'], - "/testadmin") - #ensure that signed requests do not generate HTTP traffic - self.assertEqual('', FakeHTTPConnection.last_requested_url) - - def assert_unscoped_default_tenant_auto_scopes(self, token): - """Unscoped requests with a default tenant should "auto-scope." - - The implied scope is the user's tenant ID. - - """ - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = token - body = self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - self.assertEqual(body, ['SUCCESS']) - self.assertTrue('keystone.token_info' in req.environ) - - def test_default_tenant_uuid_token(self): - self.assert_unscoped_default_tenant_auto_scopes(UUID_TOKEN_DEFAULT) - - def test_default_tenant_signed_token(self): - self.assert_unscoped_default_tenant_auto_scopes(SIGNED_TOKEN_SCOPED) - - def assert_unscoped_token_receives_401(self, token): - """Unscoped requests with no default tenant ID should be rejected.""" - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = token - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - 'Keystone uri=\'https://keystone.example.com:1234\'') - - def test_unscoped_uuid_token_receives_401(self): - self.assert_unscoped_token_receives_401(UUID_TOKEN_UNSCOPED) - - def test_unscoped_pki_token_receives_401(self): - self.assert_unscoped_token_receives_401(SIGNED_TOKEN_UNSCOPED) - - def test_revoked_token_receives_401(self): - self.middleware.token_revocation_list = self.get_revocation_list_json() - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = REVOKED_TOKEN - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - - def get_revocation_list_json(self, token_ids=None): - if token_ids is None: - token_ids = [REVOKED_TOKEN_HASH] - revocation_list = {'revoked': [{'id': x, 'expires': timeutils.utcnow()} - for x in token_ids]} - return jsonutils.dumps(revocation_list) - - def test_is_signed_token_revoked_returns_false(self): - #explicitly setting an empty revocation list here to document intent - self.middleware.token_revocation_list = jsonutils.dumps( - {"revoked": [], "extra": "success"}) - result = self.middleware.is_signed_token_revoked(REVOKED_TOKEN) - self.assertFalse(result) - - def test_is_signed_token_revoked_returns_true(self): - self.middleware.token_revocation_list = self.get_revocation_list_json() - result = self.middleware.is_signed_token_revoked(REVOKED_TOKEN) - self.assertTrue(result) - - def test_verify_signed_token_raises_exception_for_revoked_token(self): - self.middleware.token_revocation_list = self.get_revocation_list_json() - self.assertRaises(auth_token.InvalidUserToken, - self.middleware.verify_signed_token, REVOKED_TOKEN) - - def test_verify_signed_token_succeeds_for_unrevoked_token(self): - self.middleware.token_revocation_list = self.get_revocation_list_json() - self.middleware.verify_signed_token(SIGNED_TOKEN_SCOPED) - - def test_get_token_revocation_list_fetched_time_returns_min(self): - self.middleware.token_revocation_list_fetched_time = None - self.middleware.revoked_file_name = '' - self.assertEqual(self.middleware.token_revocation_list_fetched_time, - datetime.datetime.min) - - def test_get_token_revocation_list_fetched_time_returns_mtime(self): - self.middleware.token_revocation_list_fetched_time = None - mtime = os.path.getmtime(self.middleware.revoked_file_name) - fetched_time = datetime.datetime.fromtimestamp(mtime) - self.assertEqual(self.middleware.token_revocation_list_fetched_time, - fetched_time) - - def test_get_token_revocation_list_fetched_time_returns_value(self): - expected = self.middleware._token_revocation_list_fetched_time - self.assertEqual(self.middleware.token_revocation_list_fetched_time, - expected) - - def test_get_revocation_list_returns_fetched_list(self): - self.middleware.token_revocation_list_fetched_time = None - os.remove(self.middleware.revoked_file_name) - self.assertEqual(self.middleware.token_revocation_list, - REVOCATION_LIST) - - def test_get_revocation_list_returns_current_list_from_memory(self): - self.assertEqual(self.middleware.token_revocation_list, - self.middleware._token_revocation_list) - - def test_get_revocation_list_returns_current_list_from_disk(self): - in_memory_list = self.middleware.token_revocation_list - self.middleware._token_revocation_list = None - self.assertEqual(self.middleware.token_revocation_list, in_memory_list) - - def test_invalid_revocation_list_raises_service_error(self): - globals()['SIGNED_REVOCATION_LIST'] = "{}" - self.assertRaises(auth_token.ServiceError, - self.middleware.fetch_revocation_list) - - def test_fetch_revocation_list(self): - fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) - self.assertEqual(fetched_list, REVOCATION_LIST) - - def test_request_invalid_uuid_token(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = 'invalid-token' - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - 'Keystone uri=\'https://keystone.example.com:1234\'') - - def test_request_invalid_signed_token(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = INVALID_SIGNED_TOKEN - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - 'Keystone uri=\'https://keystone.example.com:1234\'') - - def test_request_no_token(self): - req = webob.Request.blank('/') - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - 'Keystone uri=\'https://keystone.example.com:1234\'') - - def test_request_no_token_log_message(self): - class FakeLog(object): - def __init__(self): - self.msg = None - self.debugmsg = None - - def warn(self, msg=None, *args, **kwargs): - self.msg = msg - - def debug(self, msg=None, *args, **kwargs): - self.debugmsg = msg - - self.middleware.LOG = FakeLog() - self.middleware.delay_auth_decision = False - self.assertRaises(auth_token.InvalidUserToken, - self.middleware._get_user_token_from_header, {}) - self.assertIsNotNone(self.middleware.LOG.msg) - self.assertIsNotNone(self.middleware.LOG.debugmsg) - - def test_request_blank_token(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = '' - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - 'Keystone uri=\'https://keystone.example.com:1234\'') - - def test_memcache(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED - self.middleware._cache = FakeMemcache() - self.middleware._use_keystone_cache = True - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.middleware._cache.set_value, None) - - def test_memcache_set_invalid(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = 'invalid-token' - self.middleware._cache = FakeMemcache() - self.middleware._use_keystone_cache = True - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.middleware._cache.set_value, "invalid") - - def test_memcache_set_expired(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED - self.middleware._cache = FakeMemcache() - self.middleware._use_keystone_cache = True - expired = datetime.datetime.now() - datetime.timedelta(minutes=1) - self.middleware._cache.token_expiration = float(expired.strftime("%s")) - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(len(self.middleware._cache.set_value), 2) - - def test_swift_memcache_set_expired(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED - self.middleware._cache = FakeSwiftMemcacheRing() - self.middleware._use_keystone_cache = False - expired = datetime.datetime.now() - datetime.timedelta(minutes=1) - self.middleware._cache.token_expiration = float(expired.strftime("%s")) - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(len(self.middleware._cache.set_value), 2) - - def test_nomemcache(self): - self.disable_module('memcache') - - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - } - - auth_token.AuthProtocol(FakeApp(), conf) - - def test_use_cache_from_env(self): - env = {'swift.cache': 'CACHE_TEST'} - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'cache': 'swift.cache', - 'memcache_servers': 'localhost:11211', - } - auth = auth_token.AuthProtocol(FakeApp(), conf) - auth._init_cache(env) - self.assertEqual(auth._cache, 'CACHE_TEST') - - def test_not_use_cache_from_env(self): - self.disable_module('memcache') - env = {'swift.cache': 'CACHE_TEST'} - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - } - auth = auth_token.AuthProtocol(FakeApp(), conf) - auth._init_cache(env) - self.assertEqual(auth._cache, None) - - def test_request_prevent_service_catalog_injection(self): - req = webob.Request.blank('/') - req.headers['X-Service-Catalog'] = '[]' - req.headers['X-Auth-Token'] = UUID_TOKEN_NO_SERVICE_CATALOG - body = self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - self.assertFalse(req.headers.get('X-Service-Catalog')) - self.assertEqual(body, ['SUCCESS']) - - def test_will_expire_soon(self): - tenseconds = datetime.datetime.utcnow() + datetime.timedelta( - seconds=10) - self.assertTrue(auth_token.will_expire_soon(tenseconds)) - fortyseconds = datetime.datetime.utcnow() + datetime.timedelta( - seconds=40) - self.assertFalse(auth_token.will_expire_soon(fortyseconds)) - - def test_encrypt_cache_data(self): - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_security_strategy': 'encrypt', - 'memcache_secret_key': 'mysecret', - } - auth = auth_token.AuthProtocol(FakeApp(), conf) - encrypted_data = \ - auth._protect_cache_value('token', - TOKEN_RESPONSES[UUID_TOKEN_DEFAULT]) - self.assertEqual('{ENCRYPT:AES256}', encrypted_data[:16]) - self.assertDictEqual( - TOKEN_RESPONSES[UUID_TOKEN_DEFAULT], - auth._unprotect_cache_value('token', encrypted_data)) - # should return None if unable to decrypt - self.assertIsNone( - auth._unprotect_cache_value('token', '{ENCRYPT:AES256}corrupted')) - self.assertIsNone( - auth._unprotect_cache_value('mykey', encrypted_data)) - - def test_sign_cache_data(self): - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_security_strategy': 'mac', - 'memcache_secret_key': 'mysecret', - } - auth = auth_token.AuthProtocol(FakeApp(), conf) - signed_data = \ - auth._protect_cache_value('mykey', - TOKEN_RESPONSES[UUID_TOKEN_DEFAULT]) - expected = '{MAC:SHA1}' - self.assertEqual( - signed_data[:10], - expected) - self.assertDictEqual( - TOKEN_RESPONSES[UUID_TOKEN_DEFAULT], - auth._unprotect_cache_value('mykey', signed_data)) - # should return None on corrupted data - self.assertIsNone( - auth._unprotect_cache_value('mykey', '{MAC:SHA1}corrupted')) - - def test_no_memcache_protection(self): - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_secret_key': 'mysecret', - } - auth = auth_token.AuthProtocol(FakeApp(), conf) - data = auth._protect_cache_value('mykey', 'This is a test!') - self.assertEqual(data, 'This is a test!') - self.assertEqual( - 'This is a test!', - auth._unprotect_cache_value('mykey', data)) - - def test_get_cache_key(self): - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_secret_key': 'mysecret', - } - auth = auth_token.AuthProtocol(FakeApp(), conf) - self.assertEqual( - 'tokens/mytoken', - auth._get_cache_key('mytoken')) - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_security_strategy': 'mac', - 'memcache_secret_key': 'mysecret', - } - auth = auth_token.AuthProtocol(FakeApp(), conf) - expected = 'tokens/' + memcache_crypt.hash_data('mytoken' + 'mysecret') - self.assertEqual(auth._get_cache_key('mytoken'), expected) - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_security_strategy': 'Encrypt', - 'memcache_secret_key': 'abc!', - } - auth = auth_token.AuthProtocol(FakeApp(), conf) - expected = 'tokens/' + memcache_crypt.hash_data('mytoken' + 'abc!') - self.assertEqual(auth._get_cache_key('mytoken'), expected) - - def test_assert_valid_memcache_protection_config(self): - # test missing memcache_secret_key - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_security_strategy': 'Encrypt', - } - self.assertRaises(Exception, auth_token.AuthProtocol, - FakeApp(), conf) - # test invalue memcache_security_strategy - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_security_strategy': 'whatever', - } - self.assertRaises(Exception, auth_token.AuthProtocol, - FakeApp(), conf) - # test missing memcache_secret_key - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_security_strategy': 'mac', - } - self.assertRaises(Exception, auth_token.AuthProtocol, - FakeApp(), conf) - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_security_strategy': 'Encrypt', - 'memcache_secret_key': '' - } - self.assertRaises(Exception, auth_token.AuthProtocol, - FakeApp(), conf) - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcache_servers': 'localhost:11211', - 'memcache_security_strategy': 'mAc', - 'memcache_secret_key': '' - } - self.assertRaises(Exception, auth_token.AuthProtocol, - FakeApp(), conf) - - -class TokenEncodingTest(testtools.TestCase): - def test_unquoted_token(self): - self.assertEqual('foo%20bar', auth_token.safe_quote('foo bar')) - - def test_quoted_token(self): - self.assertEqual('foo%20bar', auth_token.safe_quote('foo%20bar')) diff --git a/tests/test_base.py b/tests/test_base.py deleted file mode 100644 index e28b651cc..000000000 --- a/tests/test_base.py +++ /dev/null @@ -1,48 +0,0 @@ -import mock -import mox - -from keystoneclient import base -from keystoneclient import exceptions -from keystoneclient.v2_0 import roles -from tests import utils - - -class BaseTest(utils.TestCase): - - def test_resource_repr(self): - r = base.Resource(None, dict(foo="bar", baz="spam")) - self.assertEqual(repr(r), "") - - def test_getid(self): - self.assertEqual(base.getid(4), 4) - - class TmpObject(object): - id = 4 - self.assertEqual(base.getid(TmpObject), 4) - - def test_resource_lazy_getattr(self): - self.client.get = self.mox.CreateMockAnything() - self.client.get('/OS-KSADM/roles/1').AndRaise(AttributeError) - self.mox.ReplayAll() - - f = roles.Role(self.client.roles, {'id': 1, 'name': 'Member'}) - self.assertEqual(f.name, 'Member') - - # Missing stuff still fails after a second get - self.assertRaises(AttributeError, getattr, f, 'blahblah') - - def test_eq(self): - # Two resources of the same type with the same id: equal - r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) - r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) - self.assertEqual(r1, r2) - - # Two resoruces of different types: never equal - r1 = base.Resource(None, {'id': 1}) - r2 = roles.Role(None, {'id': 1}) - self.assertNotEqual(r1, r2) - - # Two resources with no ID: equal if their info is equal - r1 = base.Resource(None, {'name': 'joe', 'age': 12}) - r2 = base.Resource(None, {'name': 'joe', 'age': 12}) - self.assertEqual(r1, r2) diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index 90ab346be..000000000 --- a/tests/test_client.py +++ /dev/null @@ -1,41 +0,0 @@ -import copy -import json -import mock - -import requests - -from keystoneclient.v2_0 import client -from tests import client_fixtures -from tests import utils - - -fake_response = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(client_fixtures.PROJECT_SCOPED_TOKEN) -}) -mock_request = mock.Mock(return_value=(fake_response)) - - -class KeystoneclientTest(utils.TestCase): - - def test_scoped_init(self): - with mock.patch.object(requests, "request", mock_request): - cl = client.Client(username='exampleuser', - password='password', - auth_url='http://somewhere/') - self.assertIsNotNone(cl.auth_ref) - self.assertTrue(cl.auth_ref.scoped) - - def test_auth_ref_load(self): - with mock.patch.object(requests, "request", mock_request): - cl = client.Client(username='exampleuser', - password='password', - auth_url='http://somewhere/') - cache = json.dumps(cl.auth_ref) - new_client = client.Client(auth_ref=json.loads(cache)) - self.assertIsNotNone(new_client.auth_ref) - self.assertTrue(new_client.auth_ref.scoped) - self.assertEquals(new_client.username, 'exampleuser') - self.assertIsNone(new_client.password) - self.assertEqual(new_client.management_url, - 'http://admin:35357/v2.0') diff --git a/tests/test_ec2utils.py b/tests/test_ec2utils.py deleted file mode 100644 index b0bd4df75..000000000 --- a/tests/test_ec2utils.py +++ /dev/null @@ -1,77 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import testtools - -from keystoneclient.contrib.ec2.utils import Ec2Signer - - -class Ec2SignerTest(testtools.TestCase): - - def setUp(self): - super(Ec2SignerTest, self).setUp() - self.access = '966afbde20b84200ae4e62e09acf46b2' - self.secret = '89cdf9e94e2643cab35b8b8ac5a51f83' - self.signer = Ec2Signer(self.secret) - - def test_generate_0(self): - """Test generate function for v0 signature""" - credentials = {'host': '127.0.0.1', - 'verb': 'GET', - 'path': '/v1/', - 'params': {'SignatureVersion': '0', - 'AWSAccessKeyId': self.access, - 'Timestamp': '2012-11-27T11:47:02Z', - 'Action': 'Foo'}} - signature = self.signer.generate(credentials) - expected = 'SmXQEZAUdQw5glv5mX8mmixBtas=' - self.assertEqual(signature, expected) - - pass - - def test_generate_1(self): - """Test generate function for v1 signature""" - credentials = {'host': '127.0.0.1', - 'verb': 'GET', - 'path': '/v1/', - 'params': {'SignatureVersion': '1', - 'AWSAccessKeyId': self.access}} - signature = self.signer.generate(credentials) - expected = 'VRnoQH/EhVTTLhwRLfuL7jmFW9c=' - self.assertEqual(signature, expected) - - def test_generate_v2_SHA256(self): - """Test generate function for v2 signature, SHA256""" - credentials = {'host': '127.0.0.1', - 'verb': 'GET', - 'path': '/v1/', - 'params': {'SignatureVersion': u'2', - 'AWSAccessKeyId': self.access}} - signature = self.signer.generate(credentials) - expected = 'odsGmT811GffUO0Eu13Pq+xTzKNIjJ6NhgZU74tYX/w=' - self.assertEqual(signature, expected) - - def test_generate_v2_SHA1(self): - """Test generate function for v2 signature, SHA1""" - credentials = {'host': '127.0.0.1', - 'verb': 'GET', - 'path': '/v1/', - 'params': {'SignatureVersion': u'2', - 'AWSAccessKeyId': self.access}} - self.signer.hmac_256 = None - signature = self.signer.generate(credentials) - expected = 'ZqCxMI4ZtTXWI175743mJ0hy/Gc=' - self.assertEqual(signature, expected) diff --git a/tests/test_http.py b/tests/test_http.py deleted file mode 100644 index d9085ada2..000000000 --- a/tests/test_http.py +++ /dev/null @@ -1,96 +0,0 @@ -import mock - -import requests - -from keystoneclient import client -from keystoneclient import exceptions -from tests import utils - - -FAKE_RESPONSE = utils.TestResponse({ - "status_code": 200, - "text": '{"hi": "there"}', -}) -MOCK_REQUEST = mock.Mock(return_value=(FAKE_RESPONSE)) - - -def get_client(): - cl = client.HTTPClient(username="username", password="password", - tenant_id="tenant", auth_url="auth_test") - return cl - - -def get_authed_client(): - cl = get_client() - cl.management_url = "http://127.0.0.1:5000" - cl.auth_token = "token" - return cl - - -class ClientTest(utils.TestCase): - - def test_unauthorized_client_requests(self): - cl = get_client() - self.assertRaises(exceptions.AuthorizationFailure, cl.get, '/hi') - self.assertRaises(exceptions.AuthorizationFailure, cl.post, '/hi') - self.assertRaises(exceptions.AuthorizationFailure, cl.put, '/hi') - self.assertRaises(exceptions.AuthorizationFailure, cl.delete, '/hi') - - def test_get(self): - cl = get_authed_client() - - with mock.patch.object(requests, "request", MOCK_REQUEST): - with mock.patch('time.time', mock.Mock(return_value=1234)): - resp, body = cl.get("/hi") - headers = {"X-Auth-Token": "token", - "User-Agent": cl.USER_AGENT} - MOCK_REQUEST.assert_called_with( - "GET", - "http://127.0.0.1:5000/hi", - headers=headers, - **self.TEST_REQUEST_BASE) - # Automatic JSON parsing - self.assertEqual(body, {"hi": "there"}) - - def test_get_error(self): - cl = get_authed_client() - - fake_err_response = utils.TestResponse({ - "status_code": 400, - "text": 'Some evil plaintext string', - }) - err_MOCK_REQUEST = mock.Mock(return_value=(fake_err_response)) - - with mock.patch.object(requests, "request", err_MOCK_REQUEST): - self.assertRaises(exceptions.BadRequest, cl.get, '/hi') - - def test_post(self): - cl = get_authed_client() - - with mock.patch.object(requests, "request", MOCK_REQUEST): - cl.post("/hi", body=[1, 2, 3]) - headers = { - "X-Auth-Token": "token", - "Content-Type": "application/json", - "User-Agent": cl.USER_AGENT - } - MOCK_REQUEST.assert_called_with( - "POST", - "http://127.0.0.1:5000/hi", - headers=headers, - data='[1, 2, 3]', - **self.TEST_REQUEST_BASE) - - def test_forwarded_for(self): - ORIGINAL_IP = "10.100.100.1" - cl = client.HTTPClient(username="username", password="password", - tenant_id="tenant", auth_url="auth_test", - original_ip=ORIGINAL_IP) - - with mock.patch.object(requests, "request", MOCK_REQUEST): - res = cl.request('/', 'GET') - - args, kwargs = MOCK_REQUEST.call_args - self.assertIn( - ('Forwarded', "for=%s;by=%s" % (ORIGINAL_IP, cl.USER_AGENT)), - kwargs['headers'].items()) diff --git a/tests/test_https.py b/tests/test_https.py deleted file mode 100644 index bef1db42e..000000000 --- a/tests/test_https.py +++ /dev/null @@ -1,70 +0,0 @@ -import copy -import mock - -import requests - -from keystoneclient import client -from tests import utils - - -FAKE_RESPONSE = utils.TestResponse({ - "status_code": 200, - "text": '{"hi": "there"}', -}) -MOCK_REQUEST = mock.Mock(return_value=(FAKE_RESPONSE)) - - -def get_client(): - cl = client.HTTPClient(username="username", password="password", - tenant_id="tenant", auth_url="auth_test", - cacert="ca.pem", key="key.pem", cert="cert.pem") - return cl - - -def get_authed_client(): - cl = get_client() - cl.management_url = "https://127.0.0.1:5000" - cl.auth_token = "token" - return cl - - -class ClientTest(utils.TestCase): - - def test_get(self): - cl = get_authed_client() - - with mock.patch.object(requests, "request", MOCK_REQUEST): - with mock.patch('time.time', mock.Mock(return_value=1234)): - resp, body = cl.get("/hi") - headers = {"X-Auth-Token": "token", - "User-Agent": cl.USER_AGENT} - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['cert'] = ('cert.pem', 'key.pem') - kwargs['verify'] = 'ca.pem' - MOCK_REQUEST.assert_called_with( - "GET", - "https://127.0.0.1:5000/hi", - headers=headers, - **kwargs) - # Automatic JSON parsing - self.assertEqual(body, {"hi": "there"}) - - def test_post(self): - cl = get_authed_client() - - with mock.patch.object(requests, "request", MOCK_REQUEST): - cl.post("/hi", body=[1, 2, 3]) - headers = { - "X-Auth-Token": "token", - "Content-Type": "application/json", - "User-Agent": cl.USER_AGENT - } - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['cert'] = ('cert.pem', 'key.pem') - kwargs['verify'] = 'ca.pem' - MOCK_REQUEST.assert_called_with( - "POST", - "https://127.0.0.1:5000/hi", - headers=headers, - data='[1, 2, 3]', - **kwargs) diff --git a/tests/test_memcache_crypt.py b/tests/test_memcache_crypt.py deleted file mode 100644 index b2281d935..000000000 --- a/tests/test_memcache_crypt.py +++ /dev/null @@ -1,56 +0,0 @@ -import testtools - -from keystoneclient.middleware import memcache_crypt - - -class MemcacheCryptPositiveTests(testtools.TestCase): - def test_generate_aes_key(self): - self.assertEqual( - len(memcache_crypt.generate_aes_key('Gimme Da Key', 'hush')), 32) - - def test_compute_mac(self): - self.assertEqual( - memcache_crypt.compute_mac('mykey', 'This is a test!'), - 'tREu41yR5tEgeBWIuv9ag4AeKA8=') - - def test_sign_data(self): - expected = '{MAC:SHA1}eyJtYWMiOiAiM0FrQmdPZHRybGo1RFFESHA1eUxqcDVq' +\ - 'Si9BPSIsICJzZXJpYWxpemVkX2RhdGEiOiAiXCJUaGlzIGlzIGEgdG' +\ - 'VzdCFcIiJ9' - self.assertEqual( - memcache_crypt.sign_data('mykey', 'This is a test!'), - expected) - - def test_verify_signed_data(self): - signed = memcache_crypt.sign_data('mykey', 'Testz') - self.assertEqual( - memcache_crypt.verify_signed_data('mykey', signed), - 'Testz') - self.assertEqual( - memcache_crypt.verify_signed_data('aasSFWE13WER', 'not MACed'), - 'not MACed') - - def test_encrypt_data(self): - expected = '{ENCRYPT:AES256}' - self.assertEqual( - memcache_crypt.encrypt_data('mykey', 'mysecret', - 'This is a test!')[:16], - expected) - - def test_decrypt_data(self): - encrypted = memcache_crypt.encrypt_data('mykey', 'mysecret', 'Testz') - self.assertEqual( - memcache_crypt.decrypt_data('mykey', 'mysecret', encrypted), - 'Testz') - self.assertEqual( - memcache_crypt.decrypt_data('mykey', 'mysecret', - 'Not Encrypted!'), - 'Not Encrypted!') - - def test_no_pycrypt(self): - aes = memcache_crypt.AES - memcache_crypt.AES = None - self.assertRaises(memcache_crypt.CryptoUnavailableError, - memcache_crypt.encrypt_data, 'token', 'secret', - 'data') - memcache_crypt.AES = aes diff --git a/tests/test_service_catalog.py b/tests/test_service_catalog.py deleted file mode 100644 index 9397405f2..000000000 --- a/tests/test_service_catalog.py +++ /dev/null @@ -1,121 +0,0 @@ -from keystoneclient import exceptions -from keystoneclient import service_catalog -from tests import utils - - -# Taken directly from keystone/content/common/samples/auth.json -# Do not edit this structure. Instead, grab the latest from there. - -SERVICE_CATALOG = { - "access": { - "token": { - "id": "ab48a9efdfedb23ty3494", - "expires": "2010-11-01T03:32:15-05:00", - "tenant": { - "id": "345", - "name": "My Project" - } - }, - "user": { - "id": "123", - "name": "jqsmith", - "roles": [ - {"id": "234", - "name": "compute:admin"}, - {"id": "235", - "name": "object-store:admin", - "tenantId": "1"} - ], - "roles_links": [] - }, - "serviceCatalog": [{ - "name": "Cloud Servers", - "type": "compute", - "endpoints": [ - {"tenantId": "1", - "publicURL": "https://compute.north.host/v1/1234", - "internalURL": "https://compute.north.host/v1/1234", - "region": "North", - "versionId": "1.0", - "versionInfo": "https://compute.north.host/v1.0/", - "versionList": "https://compute.north.host/"}, - {"tenantId": "2", - "publicURL": "https://compute.north.host/v1.1/3456", - "internalURL": "https://compute.north.host/v1.1/3456", - "region": "North", - "versionId": "1.1", - "versionInfo": "https://compute.north.host/v1.1/", - "versionList": "https://compute.north.host/"} - ], - "endpoints_links": [] - }, - { - "name": "Cloud Files", - "type": "object-store", - "endpoints": [ - {"tenantId": "11", - "publicURL": "https://compute.north.host/v1/blah-blah", - "internalURL": "https://compute.north.host/v1/blah-blah", - "region": "South", - "versionId": "1.0", - "versionInfo": "uri", - "versionList": "uri"}, - {"tenantId": "2", - "publicURL": "https://compute.north.host/v1.1/blah-blah", - "internalURL": - "https://compute.north.host/v1.1/blah-blah", - "region": "South", - "versionId": "1.1", - "versionInfo": "https://compute.north.host/v1.1/", - "versionList": "https://compute.north.host/"} - ], - "endpoints_links":[{ - "rel":"next", - "href":"https://identity.north.host/v2.0/" - "endpoints?marker=2" - }] - } - ], - "serviceCatalog_links": [{ - "rel": "next", - "href": ("https://identity.host/v2.0/endpoints?" - "session=2hfh8Ar&marker=2") - }] - } -} - - -class ServiceCatalogTest(utils.TestCase): - def test_building_a_service_catalog(self): - sc = service_catalog.ServiceCatalog(SERVICE_CATALOG['access']) - - self.assertEquals(sc.url_for(service_type='compute'), - "https://compute.north.host/v1/1234") - self.assertEquals(sc.url_for('tenantId', '1', service_type='compute'), - "https://compute.north.host/v1/1234") - self.assertEquals(sc.url_for('tenantId', '2', service_type='compute'), - "https://compute.north.host/v1.1/3456") - - self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", - "South", service_type='compute') - - def test_service_catalog_endpoints(self): - sc = service_catalog.ServiceCatalog(SERVICE_CATALOG['access']) - public_ep = sc.get_endpoints(service_type='compute', - endpoint_type='publicURL') - self.assertEquals(public_ep['compute'][1]['tenantId'], '2') - self.assertEquals(public_ep['compute'][1]['versionId'], '1.1') - self.assertEquals(public_ep['compute'][1]['internalURL'], - "https://compute.north.host/v1.1/3456") - - def test_token(self): - sc = service_catalog.ServiceCatalog(SERVICE_CATALOG['access']) - - self.assertEquals(sc.get_token(), { - 'id': 'ab48a9efdfedb23ty3494', - 'tenant_id': '345', - 'user_id': '123', - 'expires': '2010-11-01T03:32:15-05:00'}) - self.assertEquals(sc.catalog['token']['expires'], - "2010-11-01T03:32:15-05:00") - self.assertEquals(sc.catalog['token']['tenant']['id'], '345') diff --git a/tests/test_shell.py b/tests/test_shell.py deleted file mode 100644 index bd8fdf09d..000000000 --- a/tests/test_shell.py +++ /dev/null @@ -1,339 +0,0 @@ -import os -import mock - -import fixtures -import requests - -from keystoneclient import shell as openstack_shell -from keystoneclient.v2_0 import shell as shell_v2_0 -from keystoneclient import exceptions -from tests import utils - - -DEFAULT_USERNAME = 'username' -DEFAULT_PASSWORD = 'password' -DEFAULT_TENANT_ID = 'tenant_id' -DEFAULT_TENANT_NAME = 'tenant_name' -DEFAULT_AUTH_URL = 'http://127.0.0.1:5000/v2.0/' - - -class ShellTest(utils.TestCase): - - FAKE_ENV = { - 'OS_USERNAME': DEFAULT_USERNAME, - 'OS_PASSWORD': DEFAULT_PASSWORD, - 'OS_TENANT_ID': DEFAULT_TENANT_ID, - 'OS_TENANT_NAME': DEFAULT_TENANT_NAME, - 'OS_AUTH_URL': DEFAULT_AUTH_URL, - } - - # Patch os.environ to avoid required auth info. - def setUp(self): - - super(ShellTest, self).setUp() - for var in self.FAKE_ENV: - self.useFixture(fixtures.EnvironmentVariable(var, - self.FAKE_ENV[var])) - - # Make a fake shell object, a helping wrapper to call it, and a quick - # way of asserting that certain API calls were made. - global shell, _shell, assert_called, assert_called_anytime - _shell = openstack_shell.OpenStackIdentityShell() - shell = lambda cmd: _shell.main(cmd.split()) - - def test_help_unknown_command(self): - self.assertRaises(exceptions.CommandError, shell, 'help foofoo') - - def test_shell_args(self): - do_tenant_mock = mock.MagicMock() - with mock.patch('keystoneclient.v2_0.shell.do_user_list', - do_tenant_mock): - shell('user-list') - assert do_tenant_mock.called - ((a, b), c) = do_tenant_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # Old_style options - shell('--os_auth_url http://0.0.0.0:5000/ --os_password xyzpdq ' - '--os_tenant_id 1234 --os_tenant_name fred ' - '--os_username barney ' - '--os_identity_api_version 2.0 user-list') - assert do_tenant_mock.called - ((a, b), c) = do_tenant_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = ('http://0.0.0.0:5000/', 'xyzpdq', '1234', - 'fred', 'barney', '2.0') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # New-style options - shell('--os-auth-url http://1.1.1.1:5000/ --os-password xyzpdq ' - '--os-tenant-id 4321 --os-tenant-name wilma ' - '--os-username betty ' - '--os-identity-api-version 2.0 user-list') - assert do_tenant_mock.called - ((a, b), c) = do_tenant_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = ('http://1.1.1.1:5000/', 'xyzpdq', '4321', - 'wilma', 'betty', '2.0') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # Test keyring options - shell('--os-auth-url http://1.1.1.1:5000/ --os-password xyzpdq ' - '--os-tenant-id 4321 --os-tenant-name wilma ' - '--os-username betty ' - '--os-identity-api-version 2.0 ' - '--os-cache ' - '--stale-duration 500 ' - '--force-new-token user-list') - assert do_tenant_mock.called - ((a, b), c) = do_tenant_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version, b.os_cache, - b.stale_duration, b.force_new_token) - expect = ('http://1.1.1.1:5000/', 'xyzpdq', '4321', - 'wilma', 'betty', '2.0', True, '500', True) - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - def test_shell_user_create_args(self): - """Test user-create args""" - do_uc_mock = mock.MagicMock() - # grab the decorators for do_user_create - uc_func = getattr(shell_v2_0, 'do_user_create') - do_uc_mock.arguments = getattr(uc_func, 'arguments', []) - with mock.patch('keystoneclient.v2_0.shell.do_user_create', - do_uc_mock): - - # Old_style options - # Test case with one --tenant_id args present: ec2 creds - shell('user-create --name=FOO ' - '--pass=secrete --tenant_id=barrr --enabled=true') - assert do_uc_mock.called - ((a, b), c) = do_uc_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - actual = (b.tenant_id, b.name, b.passwd, b.enabled) - expect = ('barrr', 'FOO', 'secrete', 'true') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # New-style options - # Test case with one --tenant-id args present: ec2 creds - shell('user-create --name=foo ' - '--pass=secrete --tenant-id=BARRR --enabled=true') - assert do_uc_mock.called - ((a, b), c) = do_uc_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - actual = (b.tenant_id, b.name, b.passwd, b.enabled) - expect = ('BARRR', 'foo', 'secrete', 'true') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # Old_style options - # Test case with --os_tenant_id and --tenant_id args present - shell('--os_tenant_id=os-tenant user-create --name=FOO ' - '--pass=secrete --tenant_id=barrr --enabled=true') - assert do_uc_mock.called - ((a, b), c) = do_uc_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'os-tenant', - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - actual = (b.tenant_id, b.name, b.passwd, b.enabled) - expect = ('barrr', 'FOO', 'secrete', 'true') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # New-style options - # Test case with --os-tenant-id and --tenant-id args present - shell('--os-tenant-id=ostenant user-create --name=foo ' - '--pass=secrete --tenant-id=BARRR --enabled=true') - assert do_uc_mock.called - ((a, b), c) = do_uc_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'ostenant', - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - actual = (b.tenant_id, b.name, b.passwd, b.enabled) - expect = ('BARRR', 'foo', 'secrete', 'true') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - def test_do_tenant_create(self): - do_tenant_mock = mock.MagicMock() - with mock.patch('keystoneclient.v2_0.shell.do_tenant_create', - do_tenant_mock): - shell('tenant-create') - assert do_tenant_mock.called - # FIXME(dtroyer): how do you test the decorators? - #shell('tenant-create --tenant-name wilma ' - # '--description "fred\'s wife"') - #assert do_tenant_mock.called - - def test_do_tenant_list(self): - do_tenant_mock = mock.MagicMock() - with mock.patch('keystoneclient.v2_0.shell.do_tenant_list', - do_tenant_mock): - shell('tenant-list') - assert do_tenant_mock.called - - def test_shell_tenant_id_args(self): - """Test a corner case where --tenant_id appears on the - command-line twice""" - do_ec2_mock = mock.MagicMock() - # grab the decorators for do_ec2_create_credentials - ec2_func = getattr(shell_v2_0, 'do_ec2_credentials_create') - do_ec2_mock.arguments = getattr(ec2_func, 'arguments', []) - with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_create', - do_ec2_mock): - - # Old_style options - # Test case with one --tenant_id args present: ec2 creds - shell('ec2-credentials-create ' - '--tenant_id=ec2-tenant --user_id=ec2-user') - assert do_ec2_mock.called - ((a, b), c) = do_ec2_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - actual = (b.tenant_id, b.user_id) - expect = ('ec2-tenant', 'ec2-user') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # New-style options - # Test case with one --tenant-id args present: ec2 creds - shell('ec2-credentials-create ' - '--tenant-id=dash-tenant --user-id=dash-user') - assert do_ec2_mock.called - ((a, b), c) = do_ec2_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - actual = (b.tenant_id, b.user_id) - expect = ('dash-tenant', 'dash-user') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # Old_style options - # Test case with two --tenant_id args present - shell('--os_tenant_id=os-tenant ec2-credentials-create ' - '--tenant_id=ec2-tenant --user_id=ec2-user') - assert do_ec2_mock.called - ((a, b), c) = do_ec2_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'os-tenant', - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - actual = (b.tenant_id, b.user_id) - expect = ('ec2-tenant', 'ec2-user') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # New-style options - # Test case with two --tenant-id args present - shell('--os-tenant-id=ostenant ec2-credentials-create ' - '--tenant-id=dash-tenant --user-id=dash-user') - assert do_ec2_mock.called - ((a, b), c) = do_ec2_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'ostenant', - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - actual = (b.tenant_id, b.user_id) - expect = ('dash-tenant', 'dash-user') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - def test_do_ec2_get(self): - do_shell_mock = mock.MagicMock() - - with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_create', - do_shell_mock): - shell('ec2-credentials-create') - assert do_shell_mock.called - - with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_get', - do_shell_mock): - shell('ec2-credentials-get') - assert do_shell_mock.called - - with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_list', - do_shell_mock): - shell('ec2-credentials-list') - assert do_shell_mock.called - - with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_delete', - do_shell_mock): - shell('ec2-credentials-delete') - assert do_shell_mock.called - - def test_do_endpoints(self): - do_shell_mock = mock.MagicMock() - # grab the decorators for do_endpoint_create - shell_func = getattr(shell_v2_0, 'do_endpoint_create') - do_shell_mock.arguments = getattr(shell_func, 'arguments', []) - with mock.patch('keystoneclient.v2_0.shell.do_endpoint_create', - do_shell_mock): - - # Old_style options - # Test create args - shell('endpoint-create ' - '--service_id=2 --publicurl=http://example.com:1234/go ' - '--adminurl=http://example.com:9876/adm') - assert do_shell_mock.called - ((a, b), c) = do_shell_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - actual = (b.service_id, b.publicurl, b.adminurl) - expect = ('2', - 'http://example.com:1234/go', - 'http://example.com:9876/adm') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # New-style options - # Test create args - shell('endpoint-create ' - '--service-id=3 --publicurl=http://example.com:4321/go ' - '--adminurl=http://example.com:9876/adm') - assert do_shell_mock.called - ((a, b), c) = do_shell_mock.call_args - actual = (b.os_auth_url, b.os_password, b.os_tenant_id, - b.os_tenant_name, b.os_username, - b.os_identity_api_version) - expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, - DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - actual = (b.service_id, b.publicurl, b.adminurl) - expect = ('3', - 'http://example.com:4321/go', - 'http://example.com:9876/adm') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 9f6c62e23..000000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,64 +0,0 @@ -from keystoneclient import exceptions -from keystoneclient import utils -from tests import utils as test_utils - - -class FakeResource(object): - pass - - -class FakeManager(object): - - resource_class = FakeResource - - resources = { - '1234': {'name': 'entity_one'}, - '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0': {'name': 'entity_two'}, - '5678': {'name': '9876'} - } - - def get(self, resource_id): - try: - return self.resources[str(resource_id)] - except KeyError: - raise exceptions.NotFound(resource_id) - - def find(self, name=None): - for resource_id, resource in self.resources.items(): - if resource['name'] == str(name): - return resource - raise exceptions.NotFound(name) - - -class FindResourceTestCase(test_utils.TestCase): - - def setUp(self): - super(FindResourceTestCase, self).setUp() - self.manager = FakeManager() - - def test_find_none(self): - self.assertRaises(exceptions.CommandError, - utils.find_resource, - self.manager, - 'asdf') - - def test_find_by_integer_id(self): - output = utils.find_resource(self.manager, 1234) - self.assertEqual(output, self.manager.resources['1234']) - - def test_find_by_str_id(self): - output = utils.find_resource(self.manager, '1234') - self.assertEqual(output, self.manager.resources['1234']) - - def test_find_by_uuid(self): - uuid = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' - output = utils.find_resource(self.manager, uuid) - self.assertEqual(output, self.manager.resources[uuid]) - - def test_find_by_str_name(self): - output = utils.find_resource(self.manager, 'entity_one') - self.assertEqual(output, self.manager.resources['1234']) - - def test_find_by_int_name(self): - output = utils.find_resource(self.manager, 9876) - self.assertEqual(output, self.manager.resources['5678']) diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 6e8dbaf85..000000000 --- a/tests/utils.py +++ /dev/null @@ -1,135 +0,0 @@ -import time - -import mox -import requests -import testtools - -from keystoneclient.v2_0 import client - - -class TestCase(testtools.TestCase): - TEST_TENANT_ID = '1' - TEST_TENANT_NAME = 'aTenant' - TEST_TOKEN = 'aToken' - TEST_USER = 'test' - TEST_ROOT_URL = 'http://127.0.0.1:5000/' - TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0') - TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' - TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0') - TEST_REQUEST_BASE = { - 'config': {'danger_mode': False}, - 'verify': True, - } - - TEST_SERVICE_CATALOG = [{ - "endpoints": [{ - "adminURL": "http://cdn.admin-nets.local:8774/v1.0", - "region": "RegionOne", - "internalURL": "http://127.0.0.1:8774/v1.0", - "publicURL": "http://cdn.admin-nets.local:8774/v1.0/" - }], - "type": "nova_compat", - "name": "nova_compat" - }, { - "endpoints": [{ - "adminURL": "http://nova/novapi/admin", - "region": "RegionOne", - "internalURL": "http://nova/novapi/internal", - "publicURL": "http://nova/novapi/public" - }], - "type": "compute", - "name": "nova" - }, { - "endpoints": [{ - "adminURL": "http://glance/glanceapi/admin", - "region": "RegionOne", - "internalURL": "http://glance/glanceapi/internal", - "publicURL": "http://glance/glanceapi/public" - }], - "type": "image", - "name": "glance" - }, { - "endpoints": [{ - "adminURL": "http://127.0.0.1:35357/v2.0", - "region": "RegionOne", - "internalURL": "http://127.0.0.1:5000/v2.0", - "publicURL": "http://127.0.0.1:5000/v2.0" - }], - "type": "identity", - "name": "keystone" - }, { - "endpoints": [{ - "adminURL": "http://swift/swiftapi/admin", - "region": "RegionOne", - "internalURL": "http://swift/swiftapi/internal", - "publicURL": "http://swift/swiftapi/public" - }], - "type": "object-store", - "name": "swift" - }] - - def setUp(self): - super(TestCase, self).setUp() - self.mox = mox.Mox() - self._original_time = time.time - time.time = lambda: 1234 - requests.request = self.mox.CreateMockAnything() - self.client = client.Client(username=self.TEST_USER, - token=self.TEST_TOKEN, - tenant_name=self.TEST_TENANT_NAME, - auth_url=self.TEST_URL, - endpoint=self.TEST_URL) - - def tearDown(self): - time.time = self._original_time - self.mox.UnsetStubs() - self.mox.VerifyAll() - super(TestCase, self).tearDown() - - -class UnauthenticatedTestCase(testtools.TestCase): - """ Class used as base for unauthenticated calls """ - TEST_ROOT_URL = 'http://127.0.0.1:5000/' - TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0') - TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' - TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0') - TEST_REQUEST_BASE = { - 'config': {'danger_mode': False}, - 'verify': True, - } - - def setUp(self): - super(UnauthenticatedTestCase, self).setUp() - self.mox = mox.Mox() - self._original_time = time.time - time.time = lambda: 1234 - requests.request = self.mox.CreateMockAnything() - - def tearDown(self): - time.time = self._original_time - self.mox.UnsetStubs() - self.mox.VerifyAll() - super(UnauthenticatedTestCase, self).tearDown() - - -class TestResponse(requests.Response): - """ Class used to wrap requests.Response and provide some - convenience to initialize with a dict """ - - def __init__(self, data): - self._text = None - super(TestResponse, self) - if isinstance(data, dict): - self.status_code = data.get('status_code', None) - self.headers = data.get('headers', None) - # Fake the text attribute to streamline Response creation - self._text = data.get('text', None) - else: - self.status_code = data - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - @property - def text(self): - return self._text diff --git a/tests/v2_0/test_auth.py b/tests/v2_0/test_auth.py deleted file mode 100644 index cd9caa56d..000000000 --- a/tests/v2_0/test_auth.py +++ /dev/null @@ -1,217 +0,0 @@ -import copy -import json - -import requests - -from keystoneclient.v2_0 import client -from keystoneclient import exceptions -from tests import utils - - -class AuthenticateAgainstKeystoneTests(utils.TestCase): - def setUp(self): - super(AuthenticateAgainstKeystoneTests, self).setUp() - self.TEST_RESPONSE_DICT = { - "access": { - "token": { - "expires": "12345", - "id": self.TEST_TOKEN, - "tenant": { - "id": self.TEST_TENANT_ID - }, - }, - "user": { - "id": self.TEST_USER - }, - "serviceCatalog": self.TEST_SERVICE_CATALOG, - }, - } - self.TEST_REQUEST_BODY = { - "auth": { - "passwordCredentials": { - "username": self.TEST_USER, - "password": self.TEST_TOKEN, - }, - "tenantId": self.TEST_TENANT_ID, - }, - } - self.TEST_REQUEST_HEADERS = { - 'Content-Type': 'application/json', - 'User-Agent': 'python-keystoneclient', - } - - def test_authenticate_failure(self): - _auth = 'auth' - _cred = 'passwordCredentials' - _pass = 'password' - self.TEST_REQUEST_BODY[_auth][_cred][_pass] = 'bad_key' - resp = utils.TestResponse({ - "status_code": 401, - "text": json.dumps({ - "unauthorized": { - "message": "Unauthorized", - "code": "401", - }, - }), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - kwargs['data'] = json.dumps(self.TEST_REQUEST_BODY) - requests.request('POST', - self.TEST_URL + "/tokens", - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - # Workaround for issue with assertRaises on python2.6 - # where with assertRaises(exceptions.Unauthorized): doesn't work - # right - def client_create_wrapper(): - client.Client(username=self.TEST_USER, - password="bad_key", - tenant_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL) - - self.assertRaises(exceptions.Unauthorized, client_create_wrapper) - - def test_auth_redirect(self): - correct_response = json.dumps(self.TEST_RESPONSE_DICT) - dict_responses = [ - { - "headers": { - 'location': self.TEST_ADMIN_URL + "/tokens", - }, - "status_code": 305, - "text": "Use proxy", - }, - { - "headers": {}, - "status_code": 200, - "text": correct_response, - }, - ] - responses = [(utils.TestResponse(resp)) - for resp in dict_responses] - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - kwargs['data'] = json.dumps(self.TEST_REQUEST_BODY) - requests.request('POST', - self.TEST_URL + "/tokens", - **kwargs).AndReturn(responses[0]) - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - kwargs['data'] = json.dumps(self.TEST_REQUEST_BODY) - requests.request('POST', - self.TEST_ADMIN_URL + "/tokens", - **kwargs).AndReturn(responses[1]) - self.mox.ReplayAll() - - cs = client.Client(username=self.TEST_USER, - password=self.TEST_TOKEN, - tenant_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL) - - self.assertEqual(cs.management_url, - self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] - ['endpoints'][0]["adminURL"]) - self.assertEqual(cs.auth_token, - self.TEST_RESPONSE_DICT["access"]["token"]["id"]) - - def test_authenticate_success_password_scoped(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_RESPONSE_DICT), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - kwargs['data'] = json.dumps(self.TEST_REQUEST_BODY) - requests.request('POST', - self.TEST_URL + "/tokens", - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - cs = client.Client(username=self.TEST_USER, - password=self.TEST_TOKEN, - tenant_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL) - self.assertEqual(cs.management_url, - self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] - ['endpoints'][0]["adminURL"]) - self.assertEqual(cs.auth_token, - self.TEST_RESPONSE_DICT["access"]["token"]["id"]) - - def test_authenticate_success_password_unscoped(self): - del self.TEST_RESPONSE_DICT['access']['serviceCatalog'] - del self.TEST_REQUEST_BODY['auth']['tenantId'] - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_RESPONSE_DICT), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - kwargs['data'] = json.dumps(self.TEST_REQUEST_BODY) - requests.request('POST', - self.TEST_URL + "/tokens", - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - cs = client.Client(username=self.TEST_USER, - password=self.TEST_TOKEN, - auth_url=self.TEST_URL) - self.assertEqual(cs.auth_token, - self.TEST_RESPONSE_DICT["access"]["token"]["id"]) - self.assertFalse('serviceCatalog' in cs.service_catalog.catalog) - - def test_authenticate_success_token_scoped(self): - del self.TEST_REQUEST_BODY['auth']['passwordCredentials'] - self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} - self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_RESPONSE_DICT), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - kwargs['data'] = json.dumps(self.TEST_REQUEST_BODY) - requests.request('POST', - self.TEST_URL + "/tokens", - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - cs = client.Client(token=self.TEST_TOKEN, - tenant_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL) - self.assertEqual(cs.management_url, - self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] - ['endpoints'][0]["adminURL"]) - self.assertEqual(cs.auth_token, - self.TEST_RESPONSE_DICT["access"]["token"]["id"]) - - def test_authenticate_success_token_unscoped(self): - del self.TEST_REQUEST_BODY['auth']['passwordCredentials'] - del self.TEST_REQUEST_BODY['auth']['tenantId'] - del self.TEST_RESPONSE_DICT['access']['serviceCatalog'] - self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} - self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_RESPONSE_DICT), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - kwargs['data'] = json.dumps(self.TEST_REQUEST_BODY) - requests.request('POST', - self.TEST_URL + "/tokens", - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - cs = client.Client(token=self.TEST_TOKEN, - auth_url=self.TEST_URL) - self.assertEqual(cs.auth_token, - self.TEST_RESPONSE_DICT["access"]["token"]["id"]) - self.assertFalse('serviceCatalog' in cs.service_catalog.catalog) diff --git a/tests/v2_0/test_ec2.py b/tests/v2_0/test_ec2.py deleted file mode 100644 index a3ac91ab9..000000000 --- a/tests/v2_0/test_ec2.py +++ /dev/null @@ -1,160 +0,0 @@ -import copy -import urlparse -import json - -import requests - -from keystoneclient.v2_0 import ec2 -from tests import utils - - -class EC2Tests(utils.TestCase): - def setUp(self): - super(EC2Tests, self).setUp() - self.TEST_REQUEST_HEADERS = { - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_POST_HEADERS = { - 'Content-Type': 'application/json', - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - - def test_create(self): - user_id = 'usr' - tenant_id = 'tnt' - req_body = { - "tenant_id": tenant_id, - } - resp_body = { - "credential": { - "access": "access", - "secret": "secret", - "tenant_id": tenant_id, - "created": "12/12/12", - "enabled": True, - } - } - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body), - }) - - url = urlparse.urljoin(self.TEST_URL, - 'v2.0/users/%s/credentials/OS-EC2' % user_id) - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_body) - requests.request('POST', - url, - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - cred = self.client.ec2.create(user_id, tenant_id) - self.assertTrue(isinstance(cred, ec2.EC2)) - self.assertEqual(cred.tenant_id, tenant_id) - self.assertEqual(cred.enabled, True) - self.assertEqual(cred.access, 'access') - self.assertEqual(cred.secret, 'secret') - - def test_get(self): - user_id = 'usr' - tenant_id = 'tnt' - resp_body = { - "credential": { - "access": "access", - "secret": "secret", - "tenant_id": tenant_id, - "created": "12/12/12", - "enabled": True, - } - } - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body), - }) - - url = urlparse.urljoin(self.TEST_URL, - 'v2.0/users/%s/credentials/OS-EC2/%s' % - (user_id, 'access')) - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - url, - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - cred = self.client.ec2.get(user_id, 'access') - self.assertTrue(isinstance(cred, ec2.EC2)) - self.assertEqual(cred.tenant_id, tenant_id) - self.assertEqual(cred.enabled, True) - self.assertEqual(cred.access, 'access') - self.assertEqual(cred.secret, 'secret') - - def test_list(self): - user_id = 'usr' - tenant_id = 'tnt' - resp_body = { - "credentials": { - "values": [ - { - "access": "access", - "secret": "secret", - "tenant_id": tenant_id, - "created": "12/12/12", - "enabled": True, - }, - { - "access": "another", - "secret": "key", - "tenant_id": tenant_id, - "created": "12/12/31", - "enabled": True, - } - ] - } - } - - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body), - }) - - url = urlparse.urljoin(self.TEST_URL, - 'v2.0/users/%s/credentials/OS-EC2' % user_id) - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - url, - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - creds = self.client.ec2.list(user_id) - self.assertTrue(len(creds), 2) - cred = creds[0] - self.assertTrue(isinstance(cred, ec2.EC2)) - self.assertEqual(cred.tenant_id, tenant_id) - self.assertEqual(cred.enabled, True) - self.assertEqual(cred.access, 'access') - self.assertEqual(cred.secret, 'secret') - - def test_delete(self): - user_id = 'usr' - access = 'access' - resp = utils.TestResponse({ - "status_code": 204, - "text": "", - }) - - url = urlparse.urljoin(self.TEST_URL, - 'v2.0/users/%s/credentials/OS-EC2/%s' % - (user_id, access)) - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('DELETE', - url, - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.ec2.delete(user_id, access) diff --git a/tests/v2_0/test_endpoints.py b/tests/v2_0/test_endpoints.py deleted file mode 100644 index 59fc0630e..000000000 --- a/tests/v2_0/test_endpoints.py +++ /dev/null @@ -1,117 +0,0 @@ -import copy -import urlparse -import json - -import requests - -from keystoneclient.v2_0 import endpoints -from tests import utils - - -class EndpointTests(utils.TestCase): - def setUp(self): - super(EndpointTests, self).setUp() - self.TEST_REQUEST_HEADERS = { - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_POST_HEADERS = { - 'Content-Type': 'application/json', - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_ENDPOINTS = { - 'endpoints': [ - { - 'adminurl': 'http://host-1:8774/v1.1/$(tenant_id)s', - 'id': '8f9531231e044e218824b0e58688d262', - 'internalurl': 'http://host-1:8774/v1.1/$(tenant_id)s', - 'publicurl': 'http://host-1:8774/v1.1/$(tenant_id)s', - 'region': 'RegionOne', - }, - { - 'adminurl': 'http://host-1:8774/v1.1/$(tenant_id)s', - 'id': '8f9531231e044e218824b0e58688d263', - 'internalurl': 'http://host-1:8774/v1.1/$(tenant_id)s', - 'publicurl': 'http://host-1:8774/v1.1/$(tenant_id)s', - 'region': 'RegionOne', - } - ] - } - - def test_create(self): - req_body = { - "endpoint": { - "region": "RegionOne", - "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", - "internalurl": "http://host-3:8774/v1.1/$(tenant_id)s", - "adminurl": "http://host-3:8774/v1.1/$(tenant_id)s", - "service_id": "e044e21", - } - } - - resp_body = { - "endpoint": { - "adminurl": "http://host-3:8774/v1.1/$(tenant_id)s", - "region": "RegionOne", - "id": "1fd485b2ffd54f409a5ecd42cba11401", - "internalurl": "http://host-3:8774/v1.1/$(tenant_id)s", - "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", - } - } - - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_body) - requests.request('POST', - urlparse.urljoin(self.TEST_URL, - 'v2.0/endpoints'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - endpoint = self.client.endpoints.create( - region=req_body['endpoint']['region'], - publicurl=req_body['endpoint']['publicurl'], - adminurl=req_body['endpoint']['adminurl'], - internalurl=req_body['endpoint']['internalurl'], - service_id=req_body['endpoint']['service_id'] - ) - self.assertTrue(isinstance(endpoint, endpoints.Endpoint)) - - def test_delete(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": "", - }) - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('DELETE', - urlparse.urljoin(self.TEST_URL, - 'v2.0/endpoints/8f953'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.endpoints.delete('8f953') - - def test_list(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_ENDPOINTS), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, - 'v2.0/endpoints'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - endpoint_list = self.client.endpoints.list() - [self.assertTrue(isinstance(r, endpoints.Endpoint)) - for r in endpoint_list] diff --git a/tests/v2_0/test_roles.py b/tests/v2_0/test_roles.py deleted file mode 100644 index f5b4ee4e6..000000000 --- a/tests/v2_0/test_roles.py +++ /dev/null @@ -1,219 +0,0 @@ -import copy -import urlparse -import json - -import requests - -from keystoneclient.v2_0 import roles -from tests import utils - - -class RoleTests(utils.TestCase): - def setUp(self): - super(RoleTests, self).setUp() - self.TEST_REQUEST_HEADERS = { - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_POST_HEADERS = { - 'Content-Type': 'application/json', - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_ROLES = { - "roles": { - "values": [ - { - "name": "admin", - "id": 1, - }, - { - "name": "member", - "id": 2, - } - ], - }, - } - - def test_create(self): - req_body = { - "role": { - "name": "sysadmin", - } - } - resp_body = { - "role": { - "name": "sysadmin", - "id": 3, - } - } - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_body) - requests.request('POST', - urlparse.urljoin(self.TEST_URL, - 'v2.0/OS-KSADM/roles'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - role = self.client.roles.create(req_body['role']['name']) - self.assertTrue(isinstance(role, roles.Role)) - self.assertEqual(role.id, 3) - self.assertEqual(role.name, req_body['role']['name']) - - def test_delete(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": "", - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('DELETE', - urlparse.urljoin(self.TEST_URL, - 'v2.0/OS-KSADM/roles/1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.roles.delete(1) - - def test_get(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps({ - 'role': self.TEST_ROLES['roles']['values'][0], - }), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, - 'v2.0/OS-KSADM/roles/1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - role = self.client.roles.get(1) - self.assertTrue(isinstance(role, roles.Role)) - self.assertEqual(role.id, 1) - self.assertEqual(role.name, 'admin') - - def test_list(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_ROLES), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, - 'v2.0/OS-KSADM/roles'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - role_list = self.client.roles.list() - [self.assertTrue(isinstance(r, roles.Role)) for r in role_list] - - def test_roles_for_user(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_ROLES), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, - 'v2.0/users/foo/roles'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - role_list = self.client.roles.roles_for_user('foo') - [self.assertTrue(isinstance(r, roles.Role)) for r in role_list] - - def test_roles_for_user_tenant(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_ROLES), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants/barrr/users/foo/roles'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - role_list = self.client.roles.roles_for_user('foo', 'barrr') - [self.assertTrue(isinstance(r, roles.Role)) for r in role_list] - - def test_add_user_role(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('PUT', - urlparse.urljoin(self.TEST_URL, - 'v2.0/users/foo/roles/OS-KSADM/barrr'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.roles.add_user_role('foo', 'barrr') - - def test_add_user_role_tenant(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('PUT', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.roles.add_user_role('foo', 'barrr', '4') - - def test_remove_user_role(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('DELETE', - urlparse.urljoin(self.TEST_URL, - 'v2.0/users/foo/roles/OS-KSADM/barrr'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.roles.remove_user_role('foo', 'barrr') - - def test_remove_user_role_tenant(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('DELETE', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.roles.remove_user_role('foo', 'barrr', '4') diff --git a/tests/v2_0/test_services.py b/tests/v2_0/test_services.py deleted file mode 100644 index 8be109a3f..000000000 --- a/tests/v2_0/test_services.py +++ /dev/null @@ -1,133 +0,0 @@ -import copy -import urlparse -import json - -import requests - -from keystoneclient.v2_0 import services -from tests import utils - - -class ServiceTests(utils.TestCase): - def setUp(self): - super(ServiceTests, self).setUp() - self.TEST_REQUEST_HEADERS = { - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_POST_HEADERS = { - 'Content-Type': 'application/json', - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_SERVICES = { - "OS-KSADM:services": { - "values": [ - { - "name": "nova", - "type": "compute", - "description": "Nova-compatible service.", - "id": 1 - }, - { - "name": "keystone", - "type": "identity", - "description": "Keystone-compatible service.", - "id": 2 - }, - ], - }, - } - - def test_create(self): - req_body = { - "OS-KSADM:service": { - "name": "swift", - "type": "object-store", - "description": "Swift-compatible service.", - } - } - resp_body = { - "OS-KSADM:service": { - "name": "swift", - "type": "object-store", - "description": "Swift-compatible service.", - "id": 3, - } - } - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_body) - requests.request('POST', - urlparse.urljoin(self.TEST_URL, - 'v2.0/OS-KSADM/services'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - service = self.client.services.create( - req_body['OS-KSADM:service']['name'], - req_body['OS-KSADM:service']['type'], - req_body['OS-KSADM:service']['description']) - self.assertTrue(isinstance(service, services.Service)) - self.assertEqual(service.id, 3) - self.assertEqual(service.name, req_body['OS-KSADM:service']['name']) - - def test_delete(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": "", - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('DELETE', - urlparse.urljoin(self.TEST_URL, - 'v2.0/OS-KSADM/services/1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.services.delete(1) - - def test_get(self): - test_services = self.TEST_SERVICES['OS-KSADM:services']['values'][0] - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps({'OS-KSADM:service': test_services}), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, - 'v2.0/OS-KSADM/services/1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - service = self.client.services.get(1) - self.assertTrue(isinstance(service, services.Service)) - self.assertEqual(service.id, 1) - self.assertEqual(service.name, 'nova') - self.assertEqual(service.type, 'compute') - - def test_list(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_SERVICES), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, - 'v2.0/OS-KSADM/services'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - service_list = self.client.services.list() - [self.assertTrue(isinstance(r, services.Service)) - for r in service_list] diff --git a/tests/v2_0/test_tenants.py b/tests/v2_0/test_tenants.py deleted file mode 100644 index 246a65ff7..000000000 --- a/tests/v2_0/test_tenants.py +++ /dev/null @@ -1,356 +0,0 @@ -import copy -import urlparse -import json - -import requests - -from keystoneclient.v2_0 import tenants -from tests import utils - - -class TenantTests(utils.TestCase): - def setUp(self): - super(TenantTests, self).setUp() - self.TEST_REQUEST_HEADERS = { - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_POST_HEADERS = { - 'Content-Type': 'application/json', - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_TENANTS = { - "tenants": { - "values": [ - { - "enabled": True, - "description": "A description change!", - "name": "invisible_to_admin", - "id": 3, - }, - { - "enabled": True, - "description": "None", - "name": "demo", - "id": 2, - }, - { - "enabled": True, - "description": "None", - "name": "admin", - "id": 1, - } - ], - "links": [], - }, - } - - def test_create(self): - req_body = { - "tenant": { - "name": "tenantX", - "description": "Like tenant 9, but better.", - "enabled": True - }, - } - resp_body = { - "tenant": { - "name": "tenantX", - "enabled": True, - "id": 4, - "description": "Like tenant 9, but better.", - } - } - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_body) - requests.request('POST', - urlparse.urljoin(self.TEST_URL, 'v2.0/tenants'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - tenant = self.client.tenants.create(req_body['tenant']['name'], - req_body['tenant']['description'], - req_body['tenant']['enabled']) - self.assertTrue(isinstance(tenant, tenants.Tenant)) - self.assertEqual(tenant.id, 4) - self.assertEqual(tenant.name, "tenantX") - self.assertEqual(tenant.description, "Like tenant 9, but better.") - - def test_delete(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": "", - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('DELETE', - urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.tenants.delete(1) - - def test_get(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps({ - 'tenant': self.TEST_TENANTS['tenants']['values'][2], - }), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - t = self.client.tenants.get(1) - self.assertTrue(isinstance(t, tenants.Tenant)) - self.assertEqual(t.id, 1) - self.assertEqual(t.name, 'admin') - - def test_list(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_TENANTS), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, 'v2.0/tenants'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - tenant_list = self.client.tenants.list() - [self.assertTrue(isinstance(t, tenants.Tenant)) for t in tenant_list] - - def test_list_limit(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_TENANTS), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants?limit=1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - tenant_list = self.client.tenants.list(limit=1) - [self.assertTrue(isinstance(t, tenants.Tenant)) for t in tenant_list] - - def test_list_marker(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_TENANTS), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants?marker=1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - tenant_list = self.client.tenants.list(marker=1) - [self.assertTrue(isinstance(t, tenants.Tenant)) for t in tenant_list] - - def test_list_limit_marker(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_TENANTS), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('GET', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants?marker=1&limit=1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - tenant_list = self.client.tenants.list(limit=1, marker=1) - [self.assertTrue(isinstance(t, tenants.Tenant)) for t in tenant_list] - - def test_update(self): - req_body = { - "tenant": { - "id": 4, - "name": "tenantX", - "description": "I changed you!", - "enabled": False, - }, - } - resp_body = { - "tenant": { - "name": "tenantX", - "enabled": False, - "id": 4, - "description": "I changed you!", - }, - } - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_body) - requests.request('POST', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants/4'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - tenant = self.client.tenants.update(req_body['tenant']['id'], - req_body['tenant']['name'], - req_body['tenant']['description'], - req_body['tenant']['enabled']) - self.assertTrue(isinstance(tenant, tenants.Tenant)) - self.assertEqual(tenant.id, 4) - self.assertEqual(tenant.name, "tenantX") - self.assertEqual(tenant.description, "I changed you!") - self.assertFalse(tenant.enabled) - - def test_update_empty_description(self): - req_body = { - "tenant": { - "id": 4, - "name": "tenantX", - "description": "", - "enabled": False, - }, - } - resp_body = { - "tenant": { - "name": "tenantX", - "enabled": False, - "id": 4, - "description": "", - }, - } - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_body) - requests.request('POST', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants/4'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - tenant = self.client.tenants.update(req_body['tenant']['id'], - req_body['tenant']['name'], - req_body['tenant']['description'], - req_body['tenant']['enabled']) - self.assertTrue(isinstance(tenant, tenants.Tenant)) - self.assertEqual(tenant.id, 4) - self.assertEqual(tenant.name, "tenantX") - self.assertEqual(tenant.description, "") - self.assertFalse(tenant.enabled) - - def test_add_user(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('PUT', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.tenants.add_user('4', 'foo', 'barrr') - - def test_remove_user(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('DELETE', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.tenants.remove_user('4', 'foo', 'barrr') - - def test_tenant_add_user(self): - req_body = { - "tenant": { - "id": 4, - "name": "tenantX", - "description": "I changed you!", - "enabled": False, - }, - } - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('PUT', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - # make tenant object with manager - tenant = self.client.tenants.resource_class(self.client.tenants, - req_body['tenant']) - tenant.add_user('foo', 'barrr') - self.assertTrue(isinstance(tenant, tenants.Tenant)) - - def test_tenant_remove_user(self): - req_body = { - "tenant": { - "id": 4, - "name": "tenantX", - "description": "I changed you!", - "enabled": False, - }, - } - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request('DELETE', - urlparse.urljoin(self.TEST_URL, - 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - # make tenant object with manager - tenant = self.client.tenants.resource_class(self.client.tenants, - req_body['tenant']) - tenant.remove_user('foo', 'barrr') - self.assertTrue(isinstance(tenant, tenants.Tenant)) diff --git a/tests/v2_0/test_tokens.py b/tests/v2_0/test_tokens.py deleted file mode 100644 index 9863b8dbd..000000000 --- a/tests/v2_0/test_tokens.py +++ /dev/null @@ -1,34 +0,0 @@ -import copy -import urlparse - -import requests - -from tests import utils - - -class TokenTests(utils.TestCase): - def setUp(self): - super(TokenTests, self).setUp() - self.TEST_REQUEST_HEADERS = { - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient'} - self.TEST_POST_HEADERS = { - 'Content-Type': 'application/json', - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient'} - - def test_delete(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": ""}) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request( - 'DELETE', - urlparse.urljoin(self.TEST_URL, 'v2.0/tokens/1'), - **kwargs).AndReturn((resp)) - - self.mox.ReplayAll() - - self.client.tokens.delete(1) diff --git a/tests/v2_0/test_users.py b/tests/v2_0/test_users.py deleted file mode 100644 index 154cea5cb..000000000 --- a/tests/v2_0/test_users.py +++ /dev/null @@ -1,298 +0,0 @@ -import copy -import urlparse -import json - -import requests - -from keystoneclient.v2_0 import users -from tests import utils - - -class UserTests(utils.TestCase): - def setUp(self): - super(UserTests, self).setUp() - self.TEST_REQUEST_HEADERS = { - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_POST_HEADERS = { - 'Content-Type': 'application/json', - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - self.TEST_USERS = { - "users": { - "values": [ - { - "email": "None", - "enabled": True, - "id": 1, - "name": "admin", - }, - { - "email": "None", - "enabled": True, - "id": 2, - "name": "demo", - }, - ] - } - } - - def test_create(self): - req_body = { - "user": { - "name": "gabriel", - "password": "test", - "tenantId": 2, - "email": "test@example.com", - "enabled": True, - } - } - resp_body = { - "user": { - "name": "gabriel", - "enabled": True, - "tenantId": 2, - "id": 3, - "password": "test", - "email": "test@example.com", - } - } - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_body) - requests.request( - 'POST', - urlparse.urljoin(self.TEST_URL, 'v2.0/users'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - user = self.client.users.create(req_body['user']['name'], - req_body['user']['password'], - req_body['user']['email'], - tenant_id=req_body['user']['tenantId'], - enabled=req_body['user']['enabled']) - self.assertTrue(isinstance(user, users.User)) - self.assertEqual(user.id, 3) - self.assertEqual(user.name, "gabriel") - self.assertEqual(user.email, "test@example.com") - - def test_delete(self): - resp = utils.TestResponse({ - "status_code": 204, - "text": "", - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request( - 'DELETE', - urlparse.urljoin(self.TEST_URL, 'v2.0/users/1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.client.users.delete(1) - - def test_get(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps({ - 'user': self.TEST_USERS['users']['values'][0], - }) - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request( - 'GET', - urlparse.urljoin(self.TEST_URL, 'v2.0/users/1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - u = self.client.users.get(1) - self.assertTrue(isinstance(u, users.User)) - self.assertEqual(u.id, 1) - self.assertEqual(u.name, 'admin') - - def test_list(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_USERS), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request( - 'GET', - urlparse.urljoin(self.TEST_URL, 'v2.0/users'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - user_list = self.client.users.list() - [self.assertTrue(isinstance(u, users.User)) for u in user_list] - - def test_list_limit(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_USERS), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request( - 'GET', - urlparse.urljoin(self.TEST_URL, 'v2.0/users?limit=1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - user_list = self.client.users.list(limit=1) - [self.assertTrue(isinstance(u, users.User)) for u in user_list] - - def test_list_marker(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_USERS), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request( - 'GET', - urlparse.urljoin(self.TEST_URL, 'v2.0/users?marker=1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - user_list = self.client.users.list(marker=1) - [self.assertTrue(isinstance(u, users.User)) for u in user_list] - - def test_list_limit_marker(self): - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(self.TEST_USERS), - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_REQUEST_HEADERS - requests.request( - 'GET', - urlparse.urljoin(self.TEST_URL, 'v2.0/users?marker=1&limit=1'), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - user_list = self.client.users.list(limit=1, marker=1) - [self.assertTrue(isinstance(u, users.User)) for u in user_list] - - def test_update(self): - req_1 = { - "user": { - "id": 2, - "email": "gabriel@example.com", - "name": "gabriel", - } - } - req_2 = { - "user": { - "id": 2, - "password": "swordfish", - } - } - req_3 = { - "user": { - "id": 2, - "tenantId": 1, - } - } - req_4 = { - "user": { - "id": 2, - "enabled": False, - } - } - - # Keystone basically echoes these back... including the password :-/ - resp_1 = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(req_1) - }) - resp_2 = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(req_2) - }) - resp_3 = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(req_3) - }) - resp_4 = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(req_4) - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_1) - requests.request( - 'PUT', - urlparse.urljoin(self.TEST_URL, 'v2.0/users/2'), - **kwargs).AndReturn((resp_1)) - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_2) - requests.request( - 'PUT', - urlparse.urljoin(self.TEST_URL, 'v2.0/users/2/OS-KSADM/password'), - **kwargs).AndReturn((resp_2)) - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_3) - requests.request( - 'PUT', - urlparse.urljoin(self.TEST_URL, 'v2.0/users/2/OS-KSADM/tenant'), - **kwargs).AndReturn((resp_3)) - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_4) - requests.request( - 'PUT', - urlparse.urljoin(self.TEST_URL, 'v2.0/users/2/OS-KSADM/enabled'), - **kwargs).AndReturn((resp_4)) - self.mox.ReplayAll() - - user = self.client.users.update(2, - name='gabriel', - email='gabriel@example.com') - user = self.client.users.update_password(2, 'swordfish') - user = self.client.users.update_tenant(2, 1) - user = self.client.users.update_enabled(2, False) - - def test_update_own_password(self): - req_body = { - 'user': { - 'password': 'ABCD', 'original_password': 'DCBA' - } - } - resp_body = { - 'access': {} - } - resp = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp_body) - }) - - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.TEST_POST_HEADERS - kwargs['data'] = json.dumps(req_body) - requests.request( - 'PATCH', - urlparse.urljoin(self.TEST_URL, 'v2.0/OS-KSCRUD/users/123'), - **kwargs).AndReturn((resp)) - - self.mox.ReplayAll() - - self.client.user_id = '123' - self.client.users.update_own_password('DCBA', 'ABCD') diff --git a/tests/v3/test_credentials.py b/tests/v3/test_credentials.py deleted file mode 100644 index 180f680dd..000000000 --- a/tests/v3/test_credentials.py +++ /dev/null @@ -1,22 +0,0 @@ -import uuid - -from keystoneclient.v3 import credentials -from tests.v3 import utils - - -class CredentialTests(utils.TestCase, utils.CrudTests): - def setUp(self): - super(CredentialTests, self).setUp() - self.additionalSetUp() - self.key = 'credential' - self.collection_key = 'credentials' - self.model = credentials.Credential - self.manager = self.client.credentials - - def new_ref(self, **kwargs): - kwargs = super(CredentialTests, self).new_ref(**kwargs) - kwargs.setdefault('data', uuid.uuid4().hex) - kwargs.setdefault('project_id', uuid.uuid4().hex) - kwargs.setdefault('type', uuid.uuid4().hex) - kwargs.setdefault('user_id', uuid.uuid4().hex) - return kwargs diff --git a/tests/v3/test_domains.py b/tests/v3/test_domains.py deleted file mode 100644 index 8cf0ea075..000000000 --- a/tests/v3/test_domains.py +++ /dev/null @@ -1,20 +0,0 @@ -import uuid - -from keystoneclient.v3 import domains -from tests.v3 import utils - - -class DomainTests(utils.TestCase, utils.CrudTests): - def setUp(self): - super(DomainTests, self).setUp() - self.additionalSetUp() - self.key = 'domain' - self.collection_key = 'domains' - self.model = domains.Domain - self.manager = self.client.domains - - def new_ref(self, **kwargs): - kwargs = super(DomainTests, self).new_ref(**kwargs) - kwargs.setdefault('enabled', True) - kwargs.setdefault('name', uuid.uuid4().hex) - return kwargs diff --git a/tests/v3/test_policies.py b/tests/v3/test_policies.py deleted file mode 100644 index 4b924be3a..000000000 --- a/tests/v3/test_policies.py +++ /dev/null @@ -1,20 +0,0 @@ -import uuid - -from keystoneclient.v3 import policies -from tests.v3 import utils - - -class PolicyTests(utils.TestCase, utils.CrudTests): - def setUp(self): - super(PolicyTests, self).setUp() - self.additionalSetUp() - self.key = 'policy' - self.collection_key = 'policies' - self.model = policies.Policy - self.manager = self.client.policies - - def new_ref(self, **kwargs): - kwargs = super(PolicyTests, self).new_ref(**kwargs) - kwargs.setdefault('type', uuid.uuid4().hex) - kwargs.setdefault('blob', uuid.uuid4().hex) - return kwargs diff --git a/tests/v3/test_projects.py b/tests/v3/test_projects.py deleted file mode 100644 index 92bdc848d..000000000 --- a/tests/v3/test_projects.py +++ /dev/null @@ -1,73 +0,0 @@ -import copy -import urlparse -import uuid - -import requests - -from keystoneclient.v3 import projects -from tests.v3 import utils - - -class ProjectTests(utils.TestCase, utils.CrudTests): - def setUp(self): - super(ProjectTests, self).setUp() - self.additionalSetUp() - self.key = 'project' - self.collection_key = 'projects' - self.model = projects.Project - self.manager = self.client.projects - - def new_ref(self, **kwargs): - kwargs = super(ProjectTests, self).new_ref(**kwargs) - kwargs.setdefault('domain_id', uuid.uuid4().hex) - kwargs.setdefault('enabled', True) - kwargs.setdefault('name', uuid.uuid4().hex) - return kwargs - - def test_list_projects_for_user(self): - ref_list = [self.new_ref(), self.new_ref()] - - user_id = uuid.uuid4().hex - resp = utils.TestResponse({ - "status-code": 200, - "text": self.serialize(ref_list), - }) - - method = 'GET' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/users/%s/%s' % (user_id, self.collection_key)), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - returned_list = self.manager.list(user=user_id) - self.assertTrue(len(returned_list)) - [self.assertTrue(isinstance(r, self.model)) for r in returned_list] - - def test_list_projects_for_domain(self): - ref_list = [self.new_ref(), self.new_ref()] - - domain_id = uuid.uuid4().hex - resp = utils.TestResponse({ - "status_code": 200, - "text": self.serialize(ref_list), - }) - - method = 'GET' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/%s?domain_id=%s' % (self.collection_key, domain_id)), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - returned_list = self.manager.list(domain=domain_id) - self.assertTrue(len(returned_list)) - [self.assertTrue(isinstance(r, self.model)) for r in returned_list] diff --git a/tests/v3/test_roles.py b/tests/v3/test_roles.py deleted file mode 100644 index d694a3ef1..000000000 --- a/tests/v3/test_roles.py +++ /dev/null @@ -1,518 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import copy -import urlparse -import uuid - -import requests - -from keystoneclient import exceptions -from keystoneclient.v3 import roles -from tests.v3 import utils - - -class RoleTests(utils.TestCase, utils.CrudTests): - def setUp(self): - super(RoleTests, self).setUp() - self.additionalSetUp() - self.key = 'role' - self.collection_key = 'roles' - self.model = roles.Role - self.manager = self.client.roles - - def new_ref(self, **kwargs): - kwargs = super(RoleTests, self).new_ref(**kwargs) - kwargs.setdefault('name', uuid.uuid4().hex) - return kwargs - - def test_domain_role_grant(self): - user_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 201, - "text": '', - }) - - method = 'PUT' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/domains/%s/users/%s/%s/%s' % ( - domain_id, user_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.grant(role=ref['id'], domain=domain_id, user=user_id) - - def test_domain_group_role_grant(self): - group_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 201, - "text": '', - }) - - method = 'PUT' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/domains/%s/groups/%s/%s/%s' % ( - domain_id, group_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.grant(role=ref['id'], domain=domain_id, group=group_id) - - def test_domain_role_list(self): - user_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref_list = [self.new_ref(), self.new_ref()] - resp = utils.TestResponse({ - "status_code": 200, - "text": self.serialize(ref_list), - }) - - method = 'GET' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/domains/%s/users/%s/%s' % ( - domain_id, user_id, self.collection_key)), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.list(domain=domain_id, user=user_id) - - def test_domain_group_role_list(self): - group_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref_list = [self.new_ref(), self.new_ref()] - resp = utils.TestResponse({ - "status_code": 200, - "text": self.serialize(ref_list), - }) - - method = 'GET' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/domains/%s/groups/%s/%s' % ( - domain_id, group_id, self.collection_key)), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.list(domain=domain_id, group=group_id) - - def test_domain_role_check(self): - user_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - method = 'HEAD' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/domains/%s/users/%s/%s/%s' % ( - domain_id, user_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.check(role=ref['id'], domain=domain_id, - user=user_id) - - def test_domain_group_role_check(self): - return - group_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - method = 'HEAD' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/domains/%s/groups/%s/%s/%s' % ( - domain_id, group_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.check(role=ref['id'], domain=domain_id, group=group_id) - - def test_domain_role_revoke(self): - user_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - method = 'DELETE' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/domains/%s/users/%s/%s/%s' % ( - domain_id, user_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.revoke(role=ref['id'], domain=domain_id, user=user_id) - - def test_domain_group_role_revoke(self): - group_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - method = 'DELETE' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/domains/%s/groups/%s/%s/%s' % ( - domain_id, group_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.revoke(role=ref['id'], domain=domain_id, group=group_id) - - def test_project_role_grant(self): - user_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 201, - "text": '', - }) - - method = 'PUT' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/projects/%s/users/%s/%s/%s' % ( - project_id, user_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.grant(role=ref['id'], project=project_id, user=user_id) - - def test_project_group_role_grant(self): - group_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 201, - "text": '', - }) - - method = 'PUT' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/projects/%s/groups/%s/%s/%s' % ( - project_id, group_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.grant(role=ref['id'], project=project_id, group=group_id) - - def test_project_role_list(self): - user_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref_list = [self.new_ref(), self.new_ref()] - resp = utils.TestResponse({ - "status_code": 200, - "text": self.serialize(ref_list), - }) - - method = 'GET' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/projects/%s/users/%s/%s' % ( - project_id, user_id, self.collection_key)), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.list(project=project_id, user=user_id) - - def test_project_group_role_list(self): - group_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref_list = [self.new_ref(), self.new_ref()] - resp = utils.TestResponse({ - "status_code": 200, - "text": self.serialize(ref_list), - }) - - method = 'GET' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/projects/%s/groups/%s/%s' % ( - project_id, group_id, self.collection_key)), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.list(project=project_id, group=group_id) - - def test_project_role_check(self): - user_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 200, - "text": '', - }) - - method = 'HEAD' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/projects/%s/users/%s/%s/%s' % ( - project_id, user_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.check(role=ref['id'], project=project_id, user=user_id) - - def test_project_group_role_check(self): - group_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 200, - "text": '', - }) - - method = 'HEAD' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/projects/%s/groups/%s/%s/%s' % ( - project_id, group_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.check(role=ref['id'], project=project_id, group=group_id) - - def test_project_role_revoke(self): - user_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - method = 'DELETE' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/projects/%s/users/%s/%s/%s' % ( - project_id, user_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.revoke(role=ref['id'], project=project_id, user=user_id) - - def test_project_group_role_revoke(self): - group_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - method = 'DELETE' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/projects/%s/groups/%s/%s/%s' % ( - project_id, group_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.revoke(role=ref['id'], project=project_id, group=group_id) - - def test_domain_project_role_grant_fails(self): - user_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref = self.new_ref() - - self.assertRaises( - exceptions.ValidationError, - self.manager.grant, - role=ref['id'], - domain=domain_id, - project=project_id, - user=user_id) - - def test_domain_project_role_list_fails(self): - user_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - - self.assertRaises( - exceptions.ValidationError, - self.manager.list, - domain=domain_id, - project=project_id, - user=user_id) - - def test_domain_project_role_check_fails(self): - user_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref = self.new_ref() - - self.assertRaises( - exceptions.ValidationError, - self.manager.check, - role=ref['id'], - domain=domain_id, - project=project_id, - user=user_id) - - def test_domain_project_role_revoke_fails(self): - user_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref = self.new_ref() - - self.assertRaises( - exceptions.ValidationError, - self.manager.revoke, - role=ref['id'], - domain=domain_id, - project=project_id, - user=user_id) - - def test_user_group_role_grant_fails(self): - user_id = uuid.uuid4().hex - group_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref = self.new_ref() - - self.assertRaises( - exceptions.ValidationError, - self.manager.grant, - role=ref['id'], - project=project_id, - group=group_id, - user=user_id) - - def test_user_group_role_list_fails(self): - user_id = uuid.uuid4().hex - group_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - - self.assertRaises( - exceptions.ValidationError, - self.manager.list, - project=project_id, - group=group_id, - user=user_id) - - def test_user_group_role_check_fails(self): - user_id = uuid.uuid4().hex - group_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref = self.new_ref() - - self.assertRaises( - exceptions.ValidationError, - self.manager.check, - role=ref['id'], - project=project_id, - group=group_id, - user=user_id) - - def test_user_group_role_revoke_fails(self): - user_id = uuid.uuid4().hex - group_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - ref = self.new_ref() - - self.assertRaises( - exceptions.ValidationError, - self.manager.revoke, - role=ref['id'], - project=project_id, - group=group_id, - user=user_id) diff --git a/tests/v3/test_services.py b/tests/v3/test_services.py deleted file mode 100644 index 545e84e1a..000000000 --- a/tests/v3/test_services.py +++ /dev/null @@ -1,21 +0,0 @@ -import uuid - -from keystoneclient.v3 import services -from tests.v3 import utils - - -class ServiceTests(utils.TestCase, utils.CrudTests): - def setUp(self): - super(ServiceTests, self).setUp() - self.additionalSetUp() - self.key = 'service' - self.collection_key = 'services' - self.model = services.Service - self.manager = self.client.services - - def new_ref(self, **kwargs): - kwargs = super(ServiceTests, self).new_ref(**kwargs) - kwargs.setdefault('name', uuid.uuid4().hex) - kwargs.setdefault('type', uuid.uuid4().hex) - kwargs.setdefault('enabled', True) - return kwargs diff --git a/tests/v3/test_users.py b/tests/v3/test_users.py deleted file mode 100644 index 480e6bfb3..000000000 --- a/tests/v3/test_users.py +++ /dev/null @@ -1,133 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import copy -import urlparse -import uuid - -import requests - -from keystoneclient.v3 import users -from tests.v3 import utils - - -class UserTests(utils.TestCase, utils.CrudTests): - def setUp(self): - super(UserTests, self).setUp() - self.additionalSetUp() - self.key = 'user' - self.collection_key = 'users' - self.model = users.User - self.manager = self.client.users - - def new_ref(self, **kwargs): - kwargs = super(UserTests, self).new_ref(**kwargs) - kwargs.setdefault('description', uuid.uuid4().hex) - kwargs.setdefault('domain_id', uuid.uuid4().hex) - kwargs.setdefault('enabled', True) - kwargs.setdefault('name', uuid.uuid4().hex) - kwargs.setdefault('project_id', uuid.uuid4().hex) - return kwargs - - def test_add_user_to_group(self): - group_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - method = 'PUT' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/groups/%s/%s/%s' % ( - group_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.add_to_group(user=ref['id'], group=group_id) - - def test_list_users_in_group(self): - group_id = uuid.uuid4().hex - ref_list = [self.new_ref(), self.new_ref()] - resp = utils.TestResponse({ - "status_code": 200, - "text": self.serialize(ref_list), - }) - - method = 'GET' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/groups/%s/%s' % ( - group_id, self.collection_key)), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - returned_list = self.manager.list(group=group_id) - self.assertTrue(len(returned_list)) - [self.assertTrue(isinstance(r, self.model)) for r in returned_list] - - def test_check_user_in_group(self): - group_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - method = 'HEAD' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/groups/%s/%s/%s' % ( - group_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.check_in_group(user=ref['id'], group=group_id) - - def test_remove_user_from_group(self): - group_id = uuid.uuid4().hex - ref = self.new_ref() - resp = utils.TestResponse({ - "status_code": 204, - "text": '', - }) - - method = 'DELETE' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/groups/%s/%s/%s' % ( - group_id, self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.remove_from_group(user=ref['id'], group=group_id) diff --git a/tests/v3/utils.py b/tests/v3/utils.py deleted file mode 100644 index e2a1412aa..000000000 --- a/tests/v3/utils.py +++ /dev/null @@ -1,270 +0,0 @@ -import copy -import json -import uuid -import time -import urlparse - -import mox -import requests -import testtools - -from keystoneclient.v3 import client - - -def parameterize(ref): - """Rewrites attributes to match the kwarg naming convention in client. - - >>> paramterize({'project_id': 0}) - {'project': 0} - - """ - params = ref.copy() - for key in ref: - if key[-3:] == '_id': - params.setdefault(key[:-3], params.pop(key)) - return params - - -class TestClient(client.Client): - - def serialize(self, entity): - return json.dumps(entity, sort_keys=True) - - -class TestCase(testtools.TestCase): - TEST_TENANT_NAME = 'aTenant' - TEST_TOKEN = 'aToken' - TEST_USER = 'test' - TEST_ROOT_URL = 'http://127.0.0.1:5000/' - TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') - TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' - TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') - TEST_REQUEST_BASE = { - 'config': {'danger_mode': False}, - 'verify': True, - } - - def setUp(self): - super(TestCase, self).setUp() - self.mox = mox.Mox() - self._original_time = time.time - time.time = lambda: 1234 - requests.request = self.mox.CreateMockAnything() - self.client = TestClient(username=self.TEST_USER, - token=self.TEST_TOKEN, - tenant_name=self.TEST_TENANT_NAME, - auth_url=self.TEST_URL, - endpoint=self.TEST_URL) - - def tearDown(self): - time.time = self._original_time - self.mox.UnsetStubs() - self.mox.VerifyAll() - super(TestCase, self).tearDown() - - -class UnauthenticatedTestCase(testtools.TestCase): - """ Class used as base for unauthenticated calls """ - TEST_ROOT_URL = 'http://127.0.0.1:5000/' - TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') - TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' - TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') - TEST_REQUEST_BASE = { - 'config': {'danger_mode': False}, - 'verify': True, - } - - def setUp(self): - super(UnauthenticatedTestCase, self).setUp() - self.mox = mox.Mox() - self._original_time = time.time - time.time = lambda: 1234 - requests.request = self.mox.CreateMockAnything() - - def tearDown(self): - time.time = self._original_time - self.mox.UnsetStubs() - self.mox.VerifyAll() - super(UnauthenticatedTestCase, self).tearDown() - - -class CrudTests(testtools.TestCase): - key = None - collection_key = None - model = None - manager = None - - def new_ref(self, **kwargs): - kwargs.setdefault('id', uuid.uuid4().hex) - return kwargs - - def additionalSetUp(self): - self.headers = { - 'GET': { - 'X-Auth-Token': 'aToken', - 'User-Agent': 'python-keystoneclient', - } - } - - self.headers['HEAD'] = self.headers['GET'].copy() - self.headers['DELETE'] = self.headers['GET'].copy() - self.headers['PUT'] = self.headers['GET'].copy() - self.headers['POST'] = self.headers['GET'].copy() - self.headers['POST']['Content-Type'] = 'application/json' - self.headers['PATCH'] = self.headers['POST'].copy() - - def serialize(self, entity): - if isinstance(entity, dict): - return json.dumps({self.key: entity}, sort_keys=True) - if isinstance(entity, list): - return json.dumps({self.collection_key: entity}, sort_keys=True) - raise NotImplementedError('Are you sure you want to serialize that?') - - def test_create(self, ref=None): - ref = ref or self.new_ref() - resp = TestResponse({ - "status_code": 201, - "text": self.serialize(ref), - }) - - method = 'POST' - req_ref = ref.copy() - req_ref.pop('id') - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - kwargs['data'] = self.serialize(req_ref) - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/%s' % self.collection_key), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - returned = self.manager.create(**parameterize(req_ref)) - self.assertTrue(isinstance(returned, self.model)) - for attr in ref: - self.assertEqual( - getattr(returned, attr), - ref[attr], - 'Expected different %s' % attr) - - def test_get(self, ref=None): - ref = ref or self.new_ref() - resp = TestResponse({ - "status_code": 200, - "text": self.serialize(ref), - }) - - method = 'GET' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/%s/%s' % (self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - returned = self.manager.get(ref['id']) - self.assertTrue(isinstance(returned, self.model)) - for attr in ref: - self.assertEqual( - getattr(returned, attr), - ref[attr], - 'Expected different %s' % attr) - - def test_list(self, ref_list=None, expected_path=None, **filter_kwargs): - ref_list = ref_list or [self.new_ref(), self.new_ref()] - resp = TestResponse({ - "status_code": 200, - "text": self.serialize(ref_list), - }) - - method = 'GET' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - expected_path or 'v3/%s' % self.collection_key), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - returned_list = self.manager.list(**filter_kwargs) - self.assertTrue(len(returned_list)) - [self.assertTrue(isinstance(r, self.model)) for r in returned_list] - - def test_update(self, ref=None): - ref = ref or self.new_ref() - req_ref = ref.copy() - del req_ref['id'] - resp = TestResponse({ - "status_code": 200, - "text": self.serialize(ref), - }) - - method = 'PATCH' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - kwargs['data'] = self.serialize(req_ref) - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/%s/%s' % (self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - returned = self.manager.update(ref['id'], **parameterize(req_ref)) - self.assertTrue(isinstance(returned, self.model)) - for attr in ref: - self.assertEqual( - getattr(returned, attr), - ref[attr], - 'Expected different %s' % attr) - - def test_delete(self, ref=None): - ref = ref or self.new_ref() - resp = TestResponse({ - "status_code": 204, - "text": '', - }) - - method = 'DELETE' - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = self.headers[method] - requests.request( - method, - urlparse.urljoin( - self.TEST_URL, - 'v3/%s/%s' % (self.collection_key, ref['id'])), - **kwargs).AndReturn((resp)) - self.mox.ReplayAll() - - self.manager.delete(ref['id']) - - -class TestResponse(requests.Response): - """ Class used to wrap requests.Response and provide some - convenience to initialize with a dict """ - - def __init__(self, data): - self._text = None - super(TestResponse, self) - if isinstance(data, dict): - self.status_code = data.get('status_code', None) - self.headers = data.get('headers', None) - # Fake the text attribute to streamline Response creation - self._text = data.get('text', None) - else: - self.status_code = data - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - @property - def text(self): - return self._text diff --git a/tools/generate_authors.sh b/tools/generate_authors.sh deleted file mode 100755 index c41f07988..000000000 --- a/tools/generate_authors.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -git shortlog -se | cut -c8- diff --git a/tools/install_venv.py b/tools/install_venv.py deleted file mode 100644 index 06dced8e5..000000000 --- a/tools/install_venv.py +++ /dev/null @@ -1,272 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2010 OpenStack, LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Installation script for Nova's development virtualenv -""" - -import optparse -import os -import subprocess -import sys -import platform - - -ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -VENV = os.path.join(ROOT, '.venv') -PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') -TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires') -PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) - - -def die(message, *args): - print >> sys.stderr, message % args - sys.exit(1) - - -def check_python_version(): - if sys.version_info < (2, 6): - die("Need Python Version >= 2.6") - - -def run_command_with_code(cmd, redirect_output=True, check_exit_code=True): - """ - Runs a command in an out-of-process shell, returning the - output of that command. Working directory is ROOT. - """ - if redirect_output: - stdout = subprocess.PIPE - else: - stdout = None - - proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) - output = proc.communicate()[0] - if check_exit_code and proc.returncode != 0: - die('Command "%s" failed.\n%s', ' '.join(cmd), output) - return (output, proc.returncode) - - -def run_command(cmd, redirect_output=True, check_exit_code=True): - return run_command_with_code(cmd, redirect_output, check_exit_code)[0] - - -class Distro(object): - - def check_cmd(self, cmd): - return bool(run_command(['which', cmd], check_exit_code=False).strip()) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if self.check_cmd('easy_install'): - print 'Installing virtualenv via easy_install...', - if run_command(['easy_install', 'virtualenv']): - print 'Succeeded' - return - else: - print 'Failed' - - die('ERROR: virtualenv not found.\n\nDevelopment' - ' requires virtualenv, please install it using your' - ' favorite package management tool') - - def post_process(self): - """Any distribution-specific post-processing gets done here. - - In particular, this is useful for applying patches to code inside - the venv.""" - pass - - -class Debian(Distro): - """This covers all Debian-based distributions.""" - - def check_pkg(self, pkg): - return run_command_with_code(['dpkg', '-l', pkg], - check_exit_code=False)[1] == 0 - - def apt_install(self, pkg, **kwargs): - run_command(['sudo', 'apt-get', 'install', '-y', pkg], **kwargs) - - def apply_patch(self, originalfile, patchfile): - run_command(['patch', originalfile, patchfile]) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.apt_install('python-virtualenv', check_exit_code=False) - - super(Debian, self).install_virtualenv() - - -class Fedora(Distro): - """This covers all Fedora-based distributions. - - Includes: Fedora, RHEL, CentOS, Scientific Linux""" - - def check_pkg(self, pkg): - return run_command_with_code(['rpm', '-q', pkg], - check_exit_code=False)[1] == 0 - - def yum_install(self, pkg, **kwargs): - run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs) - - def apply_patch(self, originalfile, patchfile): - run_command(['patch', originalfile, patchfile]) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.yum_install('python-virtualenv', check_exit_code=False) - - super(Fedora, self).install_virtualenv() - - -class Suse(Distro): - """This covers all SuSE distributions.""" - - def check_pkg(self, pkg): - return run_command_with_code(['rpm', '-q', pkg], - check_exit_code=False)[1] == 0 - - def zypper_install(self, pkg, **kwargs): - run_command(['sudo', 'zypper', '-qn', 'install', pkg], **kwargs) - - def apply_patch(self, originalfile, patchfile): - run_command(['patch', originalfile, patchfile]) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.zypper_install('python-virtualenv', check_exit_code=False) - - super(Suse, self).install_virtualenv() - - -def get_distro(): - if (os.path.exists('/etc/fedora-release') or - os.path.exists('/etc/redhat-release')): - return Fedora() - elif os.path.exists('/etc/SuSE-release'): - return Suse() - elif os.path.exists('/etc/debian_version'): - return Debian() - else: - return Distro() - - -def check_dependencies(): - get_distro().install_virtualenv() - - -def create_virtualenv(venv=VENV, no_site_packages=True): - """Creates the virtual environment and installs PIP only into the - virtual environment - """ - print 'Creating venv...', - if no_site_packages: - run_command(['virtualenv', '-q', '--no-site-packages', VENV]) - else: - run_command(['virtualenv', '-q', VENV]) - print 'done.' - print 'Installing pip in virtualenv...', - if not run_command(['tools/with_venv.sh', 'easy_install', - 'pip>1.0']).strip(): - die("Failed to install pip.") - print 'done.' - - -def pip_install(*args): - run_command(['tools/with_venv.sh', - 'pip', 'install', '--upgrade'] + list(args), - redirect_output=False) - - -def install_dependencies(venv=VENV): - print 'Installing dependencies with pip (this can take a while)...' - - # First things first, make sure our venv has the latest pip and distribute. - pip_install('pip') - pip_install('distribute') - - pip_install('-r', PIP_REQUIRES) - pip_install('-r', TEST_REQUIRES) - - # Tell the virtual env how to "import nova" - pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages", - "keystoneclient.pth") - f = open(pthfile, 'w') - f.write("%s\n" % ROOT) - - -def post_process(): - get_distro().post_process() - - -def print_help(): - help = """ - python-keystoneclient development environment setup is complete. - - python-keystoneclient development uses virtualenv to track and manage - Python dependencies while in development and testing. - - To activate the python-keystoneclient virtualenv for the extent of your - current shell session you can run: - - $ source .venv/bin/activate - - Or, if you prefer, you can run commands in the virtualenv on a case by case - basis by running: - - $ tools/with_venv.sh - - Also, make test will automatically use the virtualenv. - """ - print help - - -def parse_args(): - """Parse command-line arguments""" - parser = optparse.OptionParser() - parser.add_option( - "-n", "--no-site-packages", dest="no_site_packages", - default=False, action="store_true", - help="Do not inherit packages from global Python install") - return parser.parse_args() - - -def main(argv): - (options, args) = parse_args() - check_python_version() - check_dependencies() - create_virtualenv(no_site_packages=options.no_site_packages) - install_dependencies() - post_process() - print_help() - -if __name__ == '__main__': - main(sys.argv) diff --git a/tools/keystone.bash_completion b/tools/keystone.bash_completion deleted file mode 100644 index d18668b99..000000000 --- a/tools/keystone.bash_completion +++ /dev/null @@ -1,27 +0,0 @@ -# bash completion for openstack keystone - -_keystone_opts="" # lazy init -_keystone_flags="" # lazy init -_keystone_opts_exp="" # lazy init -_keystone() -{ - local cur prev kbc - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - - if [ "x$_keystone_opts" == "x" ] ; then - kbc="`keystone bash-completion | sed -e "s/ -h / /"`" - _keystone_opts="`echo "$kbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/[ ][ ]*/ /g"`" - _keystone_flags="`echo " $kbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/[ ][ ]*/ /g"`" - _keystone_opts_exp="`echo $_keystone_opts | sed -e "s/[ ]/|/g"`" - fi - - if [[ " ${COMP_WORDS[@]} " =~ " "($_keystone_opts_exp)" " && "$prev" != "help" ]] ; then - COMPREPLY=($(compgen -W "${_keystone_flags}" -- ${cur})) - else - COMPREPLY=($(compgen -W "${_keystone_opts}" -- ${cur})) - fi - return 0 -} -complete -F _keystone keystone diff --git a/tools/pip-requires b/tools/pip-requires deleted file mode 100644 index f93089ee8..000000000 --- a/tools/pip-requires +++ /dev/null @@ -1,5 +0,0 @@ -argparse -iso8601>=0.1.4 -prettytable -requests>=0.8.8,<1.0 -simplejson diff --git a/tools/test-requires b/tools/test-requires deleted file mode 100644 index d0abb3298..000000000 --- a/tools/test-requires +++ /dev/null @@ -1,18 +0,0 @@ -distribute>=0.6.24 - -coverage -fixtures -keyring -mock -mox -nose -nose-exclude -openstack.nose_plugin -nosehtmloutput -pep8==1.3.3 -pycrypto -sphinx>=1.1.2 -testtools>=0.9.22 -WebOb>=1.0.8 - -Babel diff --git a/tools/with_venv.sh b/tools/with_venv.sh deleted file mode 100755 index c8d2940fc..000000000 --- a/tools/with_venv.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -TOOLS=`dirname $0` -VENV=$TOOLS/../.venv -source $VENV/bin/activate && $@ diff --git a/tox.ini b/tox.ini index 6533a18de..e69fc3559 100644 --- a/tox.ini +++ b/tox.ini @@ -1,26 +1,100 @@ [tox] -envlist = py26,py27,pep8 +minversion = 3.18.0 +skipsdist = True +envlist = py3,pep8,releasenotes +ignore_basepython_conflict = True [testenv] -setenv = VIRTUAL_ENV={envdir} - NOSE_WITH_OPENSTACK=1 - NOSE_OPENSTACK_COLOR=1 - NOSE_OPENSTACK_RED=0.05 - NOSE_OPENSTACK_YELLOW=0.025 - NOSE_OPENSTACK_SHOW_ELAPSED=1 - NOSE_OPENSTACK_STDOUT=1 -deps = -r{toxinidir}/tools/pip-requires - -r{toxinidir}/tools/test-requires -commands = nosetests {posargs} - -[tox:jenkins] -downloadcache = ~/cache/pip +usedevelop = True +setenv = OS_STDOUT_NOCAPTURE=False + OS_STDERR_NOCAPTURE=False + +deps = + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = find . -type f -name "*.pyc" -delete + stestr run --slowest {posargs} +allowlist_externals = find +basepython = python3 [testenv:pep8] -commands = pep8 --repeat --show-source --ignore=E711,E712,E125,E126 --exclude=.venv,.tox,dist,doc . +commands = + flake8 + bandit -r keystoneclient -x tests -n5 -[testenv:cover] -setenv = NOSE_WITH_COVERAGE=1 +[testenv:bandit] +# NOTE(browne): This is required for the integration test job of the bandit +# project. Please do not remove. +commands = bandit -r keystoneclient -x tests -n5 [testenv:venv] commands = {posargs} + +[testenv:cover] +setenv = + PYTHON=coverage run --source keystoneclient --parallel-mode +commands = + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report + +[testenv:debug] +commands = oslo_debug_helper -t keystoneclient/tests {posargs} + +[testenv:functional] +setenv = {[testenv]setenv} + OS_TEST_PATH=./keystoneclient/tests/functional +passenv = OS_* + +[flake8] +# D100: Missing docstring in public module +# D101: Missing docstring in public class +# D102: Missing docstring in public method +# D103: Missing docstring in public function +# D104: Missing docstring in public package +# D107: Missing docstring in __init__ +# D203: 1 blank line required before class docstring (deprecated in pep257) +# D401 First line should be in imperative mood; try rephrasing +# W504 line break after binary operator +ignore = D100,D101,D102,D103,D104,D107,D203,D401,W504 +show-source = True +exclude = .venv,.tox,dist,doc,*egg,build + +[testenv:docs] +commands = sphinx-build -W -b html doc/source doc/build/html +deps = + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt + -r{toxinidir}/requirements.txt + +[testenv:pdf-docs] +deps = {[testenv:docs]deps} +allowlist_externals = + make + rm +commands = + rm -rf doc/build/pdf + sphinx-build -W -b latex doc/source doc/build/pdf + make -C doc/build/pdf + +[testenv:releasenotes] +commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html +deps = + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt + +[hacking] +import_exceptions = + keystoneclient.i18n + +[testenv:bindep] +# Do not install any requirements. We want this to be fast and work even if +# system dependencies are missing, since it's used to tell you what system +# dependencies are missing! This also means that bindep must be installed +# separately, outside of the requirements files. +deps = bindep +commands = bindep test +